DVWA入门靶场实战(一)

1. 导言

DVWA是一个存在安全漏洞的PHP/Mysql入门应用程序,包含了 OWASP TOP10 的所有攻击漏洞的练习环境。主要目标是帮助安全专业人员在合法环境下测试他们的技能和工具,帮助 Web 开发人员更好地理解 Web 应用程序的安全流程。

DVWA是开源的,代码由Github托管。对于安全新手来说是个很好的学习平台了。

DVWA一共包含了十个攻击模块:

  • Brute Force(暴力破解)
  • Command Injection(命令行注入)
  • CSRF(跨站请求伪造)
  • File Inclusion(文件包含)
  • File Upload(文件上传)
  • Insecure CAPTCHA (不安全的验证码)
  • SQL Injection(SQL注入)
  • SQL Injection(Blind)(SQL盲注)
  • XSS(Reflected)(反射型跨站脚本)
  • XSS(Stored)(存储型跨站脚本)

每个模块分四个等级:级别越高,安全防护越严格,渗透难度越大:

  • Low (基本没有做防护或者只是最简单的防护)
  • Medium (使用到一些非常粗糙的防护)
  • High (大大提高防护级别)
  • Impossible (基本是不可能渗透成功的,源码一般可以被参考作为生产环境 Web 防护的最佳手段)

DVWA下载地址:https://github.com/digininja/DVWA

2. DVWA环境搭建

2.1 phpStudy环境搭建

phpStudy是一款专为Windows 用户打造的免费PHP 开发环境集成工具

下载链接:https://pan.baidu.com/s/1NRpSfY1KmmwN977hwm3iHw 提取码: vvw8

下载后解压安装到指定目录,并打开执行,开启APache和MySQL服务即可:

如果你的MySQL服务开启失败,那是因为之前安装过MySQL起冲突了,那么打开计算机-服务关闭之前的服务:

2.2 靶场配置

接下来将DVWA的目录解压到phpStudy/WWW的目录中,接着进入DVWA/config目录,有一个config.inc.php.dist文件,复制一份命名为config.inc.php:

打开config.inc.php文件,修改数据库的账号和密码:

打开phpStudy面板-数据库-root-操作-修改密码,改成123456即可:

打开phpStudy-数据库-创建数据库:

数据库创建好之后,网站-创建网站,域名设置为localhost,端口随便设置一个即可,根目录指定到DVWA:

2.3 靶场开启

本地环境搭建好之后,就可以访问DVWA项目了,输入http://localhost:8085进入登录界面:

初始账号:admin

初始密码:password

登录进去以后,滑到最下面,点击Create/Reset Database创建数据库,再次登录即可显示靶场主页:

打开DVWA Security选项,可以调整难度级别:

靶场项目的源码存放于DVWA/vulnerabilities目录下,对应各种类型各种难度的代码:

3. Brute Force(暴力破解)

这一关的目标是:提供了一个简单的登录框,通过脚本结合字典爆破出密码并登录成功

3.1 Low级别

这一级别完全没有任何的防御,使用基本的工具就可以爆破成功。

源码分析:

<?php
if( isset( $_GET[ 'Login' ] ) ) {
	// Get username
	$user = $_GET[ 'username' ];
	// Get password
	$pass = $_GET[ 'password' ];
	$pass = md5( $pass );
	// Check the database
	$query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
	$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

	if( $result && mysqli_num_rows( $result ) == 1 ) {
		// Get users details
		$row    = mysqli_fetch_assoc( $result );
		$avatar = $row["avatar"];
		// Login successful
		$html .= "<p>Welcome to the password protected area {$user}</p>";
		$html .= "<img src=\"{$avatar}\" />";
	}
	else {
		// Login failed
		$html .= "<pre><br />Username and/or password incorrect.</pre>";
	}
	((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>

通过源码可以看到的问题:登录请求是GET方式,且用户名和密码都没有进行过滤

GET方式提交数据很容易暴露敏感信息,我们输入账号和密码后,URL地址栏就会暴露出来:

http://localhost:8085/vulnerabilities/brute/?username=admin&password=123456&Login=Login#

那么可以输入万能密码(SQL注入)来绕过登录:

?username=admin'--+&password=111&Login=Login#

这一关不是SQL注入,那我们就尝试也可以进行BurpSuite爆破,打开Burp-代理-内嵌浏览器,登录DVWA随便输入一个账号密码,把拦截打开进行抓包:

拦截到的请求是GET方式传输,而且账号和密码就在URL中,明文传输的。然后发送到Intruder模块进行爆破,攻击类型选择“集束炸弹-多个payload集合”

把username和password的参数值选中-添加payload位置:

打开payload选项卡,设置位置和类型,由于列表中的用户名和密码的组合项过于庞大,比较费时,所以这里就输入几个值测试一下:

可以看到响应的长度其他的都是5116,只有admin和password的组合返回了5153,所以返回的响应信息自然也不一样,那么来测试一下,登录成功:

3.2 Medium级别

Medium级别在Low的基础上做了两点防护:

  • 对用户名和密码做了转义
  • 如果输入错误,等待2秒返回响应结果
$user = $_GET[ 'username' ];
	$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

	// Sanitise password input
	$pass = $_GET[ 'password' ];
	$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
	$pass = md5( $pass );
if( $result && mysqli_num_rows( $result ) == 1 ) {
   //登录成功
}else{
   //登录失败
   sleep( 2 );//延迟2秒
   $html .= "<pre><br />Username and/or password incorrect.</pre>";
}

mysqli_real_escape_string函数的作用就是转义危险的字符串,防止进行SQL注入;

转义的意思就是说万能密码不能用了,但是仍然可以进行Burp爆破,就是时间会久点,步骤同Low级别。

3.3 High级别

High级别在Medium级别的基础上又多了两点防护:

  • 为了防止CSRF攻击,表单中增加了隐藏的token参数;
  • 如果登录失败,等待随机0~3秒返回结果
if( isset( $_GET[ 'Login' ] ) ) {
	// Check Anti-CSRF token 增加了token检测
	checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

	// Sanitise username input
	$user = $_GET[ 'username' ];
	$user = stripslashes( $user );
	$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

	// Sanitise password input
	$pass = $_GET[ 'password' ];
	$pass = stripslashes( $pass );
	$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
	$pass = md5( $pass );

	// Check database
	$query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
	$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

	if( $result && mysqli_num_rows( $result ) == 1 ) {
		// Get users details
		$row    = mysqli_fetch_assoc( $result );
		$avatar = $row["avatar"];

		// Login successful
		$html .= "<p>Welcome to the password protected area {$user}</p>";
		$html .= "<img src=\"{$avatar}\" />";
	}
	else {
		// Login failed
		sleep( rand( 0, 3 ) );//随机等待0~3秒
		$html .= "<pre><br />Username and/or password incorrect.</pre>";
	}
}

这一级别添加了token参数,只是增加了爆破的复杂度,还是可以通过BurpSuite爆破的,利用Burp抓包发现多了一个user_token的参数:

然后尝试多次输入账号密码登录,发现user_token的值是变化的,每次都不一样,然后把请求发送到重放器,可以看到user_token的值更新了,那么猜测一下返回的响应包里携带的是下一次请求的user_token值,那么可以构造一下看看能不能响应成功,只要下一次请求携带了正确的token值就可以进行爆破了,先发送到Intruder:

攻击类型设置为交叉模式,password和user_token两个参数都添加payload位置:

username和password的payload和Low级别的操作一样,主要看一下user_token的payload设置,类型设置为递归提取,点击最右侧的设置-检索-提取:点击添加:

找到user_token的值选中,点击确认即可:

找到重定向选项,选择“总是”:

然后回到payload的选项卡,添加到初始的payload:

然后找到资源池-新建资源池,最大并发请求数更改为1:

设置好了以后就可以进行爆破了,还是看长度,长度不同的优先测试:

3.4 Impossible级别

<?php

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

	// Sanitise username input
	$user = $_POST[ 'username' ];
	$user = stripslashes( $user );
	$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

	// Sanitise password input
	$pass = $_POST[ 'password' ];
	$pass = stripslashes( $pass );
	$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
	$pass = md5( $pass );

	// Default values
	$total_failed_login = 3;
	$lockout_time       = 15;
	$account_locked     = false;

	// Check the database (Check user information)
	$data = $db->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!
		//$html .= "<pre><br />This account has been locked due to too many incorrect logins.</pre>";

		// Calculate when the user would be allowed to login again
		$last_login = strtotime( $row[ 'last_login' ] );
		$timeout    = $last_login + ($lockout_time * 60);
		$timenow    = time();

		/*
		print "The last login was: " . date ("h:i:s", $last_login) . "<br />";
		print "The timenow is: " . date ("h:i:s", $timenow) . "<br />";
		print "The timeout is: " . date ("h:i:s", $timeout) . "<br />";
		*/

		// Check to see if enough time has passed, if it hasn't locked the account
		if( $timenow < $timeout ) {
			$account_locked = true;
			// print "The account is locked<br />";
		}
	}

	// 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
		$html .= "<p>Welcome to the password protected area <em>{$user}</em></p>";
		$html .= "<img src=\"{$avatar}\" />";

		// Had the account been locked out since last login?
		if( $failed_login >= $total_failed_login ) {
			$html .= "<p><em>Warning</em>: Someone might of been brute forcing your account.</p>";
			$html .= "<p>Number of login attempts: <em>{$failed_login}</em>.<br />Last login attempt was at: <em>{$last_login}</em>.</p>";
		}

		// 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
		$html .= "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, the account has been locked because of too many failed logins.<br />If this is the case, <em>please try again in {$lockout_time} minutes</em>.</pre>";

		// 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();

?>

Impossible级别不仅有之前的防御手段,还把请求方式设置成了POST,而且登录的次数和时间都有限制,登录失败超过3次,账户就被锁定;等待15分钟以后才可以重新尝试,简直的地狱难度了。

4. Command Injection(命令注入)

这一关的目标是在目标主机上执行任意指令,属于高危漏洞

4.1 Low级别

Low级别不过滤,直接使用ping命令拼接字符串就可以执行:

if( isset( $_POST[ 'Submit' ]  ) ) {
	// Get input
	$target = $_REQUEST[ 'ip' ];

	// 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
	$html .= "<pre>{$cmd}</pre>";
}

根据输入框的提示,可以直接输入IP地址测试,和WIndows中ping的返回结果一样:

如果是在WIndows系统下,这里会显示乱码,因为底层中文是GBK编码,解决方式:

找到../DVWA/dvwa/includes目录下的dvwaPage.inc.php文件,找到这一行代码:

Header( 'Content-Type: text/html;charset=utf-8' ); 
//改成GBK或者GB2312
Header( 'Content-Type: text/html;charset=GB2312' ); 

那么既然可以执行成功,就可以自己拼接指令执行了:

127.0.0.1;ipconfig
127.0.0.1 & ipconfig
127.0.0.1 && ipconfig
127.0.0.1 | ipconfig
127.0.0.1 || ipconfig

4.2 Medium级别

Medium级别在Low的基础上过滤了“&&”和“;”两种情况:

// Set blacklist
	$substitutions = array(
		'&&' => '',
		';'  => '',
	);

	// Remove any of the characters in the array (blacklist).
	$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

那么还有三种情况可以使用:

127.0.0.1 & ipconfig
127.0.0.1 | ipconfig
127.0.0.1 || ipconfig

4.3 High级别

High级别又过滤了更多的字符,好像都过滤了,而且还过滤了“-”符号,所以带参数的命令执行不了:

// Set blacklist
	$substitutions = array(
		'||' => '',
		'&'  => '',
		';'  => '',
		'| ' => '',
		'-'  => '',
		'$'  => '',
		'('  => '',
		')'  => '',
		'`'  => '',
	);
	// Remove any of the characters in the array (blacklist).
	$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

仔细一看过滤的不是管道符|,而是|空格,所以|依然可以用:

127.0.0.1 |ipconfig

4.4 Impossible级别

这里就是白名单过滤方式了,白名单比黑名单更安全,这种处理方式是判断IPV4的健壮性,是否符合IPV4的格式。

// 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
		$html .= "<pre>{$cmd}</pre>";
	}

5. CSRF(跨站请求伪造)

CSRF的原理是:攻击者通过伪装成受信任的用户,利用用户的身份在已登录的web程序内进行任意操作。

CSRF攻击的核心就在于:伪造请求

CSRF攻击需要满足的条件:

  • 用户已登录受信任的网站,并在浏览器中存储了会话Cookie
  • 用户访问了攻击者构造的恶意页面
  • 服务端未做任何校验或其他防御措施,比如未验证请求来源、未验证token

CSRF攻击的常规流程:

  • 用户登录受信任的Web应用程序(网站),生成会话Cookie
  • 用户访问了恶意页面,恶意页面通过伪造的请求向受信任网站发送操作的命令
  • 受新人网站验证Cookie的有效性,执行攻击者的操作

这一关的攻击目标是:靶场提供了一个修改密码的表单,在目标主机毫不知情的情况下,使其更改密码:

5.1 Low级别

Low级别对CSRF没有防御措施,而且是GET方式提交请求:

// Get input
	$pass_new  = $_GET[ 'password_new' ];
	$pass_conf = $_GET[ 'password_conf' ];

	// Do the passwords match?
	if( $pass_new == $pass_conf ) {
		// They do!
		$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
		$pass_new = md5( $pass_new );

		// Update the database
		$current_user = dvwaCurrentUser();
		$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . $current_user . "';";
		$result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

		// Feedback for the user
		$html .= "<pre>Password Changed.</pre>";
	}

从源码中可以看到,两次密码输入一致的话,直接带入函数修改成功:

http://localhost:8085/vulnerabilities/csrf/?password_new=123456&password_conf=123456&Change=Change#

这个链接一般的人是不会轻易点击的,可以通过短链接伪装一下,网上能搜到一大堆短链接在线生成工具,可以自行尝试一下。

5.2 Medium级别

这个级别在Low级别的基础上增加了referer的判断:

if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
   ...
}

收到请求以后,判断HTTP_REFERER请求头是否包含了服务器的名称SERVER_NAME,stripos函数是一个简单检测字符串的函数,那么HTTP_REFERER请求头里只要有服务器的域名就可以通过检测。我们通过Burp抓一下包,看一下请求报文:

抓了一下包发现没有referer,那我们自己添加一个本机的地址就可以更改成功了:

5.3 High级别

High级别增加了token检测,这和第一关的原理类似,表单中携带了一个隐藏的参数token,也就是说发起CSRF攻击的时候需要知道token的值才能发送,这个过程之前已经演示过了:

checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );

还是一样,通过Burp执行构造的链接,发送请求-抓包:

将新的token带入请求链接就可以发起CSRF攻击了:

5.4 Impossible级别

// Get input
	$pass_curr = $_GET[ 'password_current' ];//要求输入当前的密码
	$pass_new  = $_GET[ 'password_new' ];
	$pass_conf = $_GET[ 'password_conf' ];
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );

这个级别的防护方式是需要提供原密码,在无法知道原密码的情况下,是无法发起CSRF攻击的。


免责声明:


1. 一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。

2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。

3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
觉得有帮助可以赞赏本文哦~万分感谢!
文章:DVWA入门靶场实战(一)
作者:沛旗
链接:https://www.peiqiblog.com/article/5109/
版权声明::本博客站点所有文章除特别声明外,均采用 CC BY-NC-SA 4.0协议
转载请注明文章地址及作者哦~
暂无评论

发送评论(禁止发表一切违反法律法规的敏感言论) 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇