
파일 업로드 기능이 제공되는 경우,
파일의 크기와 개수, 종류를 제한하지 않고, 외부에서 접근 가능한 경로에 업로드 파일을 저장하는 경우 발생

webshell opensource project 로 검색하면 다양한 언어로 만들어진 웹쉘 파일을 쉽게 구할 수 있음
예) https://github.com/tennc/webshell
파일 업로드 기능이 제공을 확인하는 방법
<form enctype="multipart/form-data" method="post" ... > ⇐ https://developer.mozilla.org/ko/docs/Web/HTML/Element/form
<input type="file" ... >
</form>

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
허용 목록 (화이트 리스트) 방식으로 제한
⇒ 파일 종류를 비교하는 방법




┌──(kali㉿kali)-[~]
└─$ cd /usr/share/webshells
┌──(kali㉿kali)-[/usr/share/webshells]
└─$ ls
asp aspx cfm jsp laudanum perl php
┌──(kali㉿kali)-[/usr/share/webshells]
└─$ cd php
┌──(kali㉿kali)-[/usr/share/webshells/php]
└─$ ls
findsocket php-backdoor.php php-reverse-shell.php qsd-php-backdoor.php simple-backdoor.php

/usr/share/webshells/php/simple-backdoor.php 파일을 업로드 ⇒ here 마우스를 올려 보면 링크 정보가 출력됨 ⇒ 정상적으로 업로드된 것을 확인할 수 있음
cmd 요청 파라미터에 서버에서 실행할 명령어를 전달 ⇒ 명령어의 실행 결과가 화면에 출력

bee@bee-box:~$ sudo gedit /var/www/bWAPP/unrestricted_file_upload.php
<?php
include("security.php");
include("security_level_check.php");
include("functions_external.php");
include("selections.php");
if(isset($_POST["form"]))
{
$file_error = "";
switch($_COOKIE["security_level"])
{
case "0" : ⇐ https://www.php.net/manual/en/function.move-uploaded-file.php
move_uploaded_file($_FILES["file"]["tmp_name"], "images/" . $_FILES["file"]["name"]);
break; ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
업로드 파일을 현재 디렉터리의 images 아래에 업로드 파일명으로 저장
case "1" :
$file_error = file_upload_check_1($_FILES["file"]);
if(!$file_error)
{
move_uploaded_file($_FILES["file"]["tmp_name"], "images/" . $_FILES["file"]["name"]);
}
break;
case "2" :
$file_error = file_upload_check_2($_FILES["file"], array("jpg","png"));
if(!$file_error)
{
move_uploaded_file($_FILES["file"]["tmp_name"], "images/" . $_FILES["file"]["name"]);
}
break;
default :
move_uploaded_file($_FILES["file"]["tmp_name"],"images/" . $_FILES["file"]["name"]);
break;
}
}
?>
... 생략 ...
<?php
if(isset($_POST["form"]))
{
if(!$file_error)
{
echo "The image has been uploaded <a href=\"images/" . $_FILES["file"]["name"] . "\" target=\"_blank\">here</a>.";
}
else
{
echo "<font color=\"red\">" . $file_error . "</font>";
}
}
?>
function file_upload_check_1($file, $file_extensions = array("asp", "aspx", "dll", "exe", "jsp", "php"), $directory = "images")
{
$file_error = "";
// Checks if the input field is empty
if($file["name"] == "")
{
$file_error = "Please select a file...";
return $file_error;
}
// Checks if there is an error with the file
switch($file["error"])
// URL: http://php.net/manual/en/features.file-upload.errors.php
{
case 1 : $file_error = "Sorry, the file is too large. Please try again...";
break;
case 2 : $file_error = "Sorry, the file is too large. Please try again...";
break;
case 3 : $file_error = "Sorry, the file was only partially uploaded. Please try again...";
break;
case 6 : $file_error = "Sorry, a temporary folder is missing. Please try again...";
break;
case 7 : $file_error = "Sorry, the file could not be written. Please try again...";
break;
case 8 : $file_error = "Sorry, a PHP extension stopped the file upload. Please try again...";
break;
}
if($file_error)
{
return $file_error;
}
// Breaks the file in pieces (.) All pieces are put in an array
$file_array = explode(".", $file["name"]); ⇐ 확장자 검증을 위해서 .을 기준으로 분리
// Puts the last part of the array (= the file extension) in a new variabele
// Converts the characters to lower case
$file_extension = strtolower($file_array[count($file_array) - 1]); ⇐ 확장자를 소문자로 변환
// Searches if the file extension exists in the 'allowed' file extensions array
if(in_array($file_extension, $file_extensions)) ⇐ 제한 목록에 포함되어 있는 경우 오류 처리
{
$file_error = "Sorry, the file extension is not allowed. The following extensions are blocked: <b>" . join(", ", $file_extensions) . "</b>";
return $file_error;
}
// Checks if the file already exists in the directory
if(is_file("$directory/" . $file["name"])) ⇐ 동일한 파일이 존재하는지 검증
{
$file_error = "Sorry, the file already exists. Please rename the file...";
}
return $file_error;
}
┌──(kali㉿kali)-[/usr/share/webshells/php]
└─$ sudo cp simple-backdoor.php simple-backdoor.php3




function file_upload_check_2($file, $file_extensions = array("jpeg", "jpg", "png", "gif"), $directory = "images")
{
... 생략 ...
// Searches if the file extension exists in the 'allowed' file extensions array
if(!in_array($file_extension, $file_extensions)) ⇐ 화이트 리스트에 포함되지 않은 확장자인 경우 오류 처리
{
$file_error = "Sorry, the file extension is not allowed. Only the following extensions are allowed: <b>" . join(", ", $file_extensions) . "</b>";
return $file_error;
}
... 생략 ...




<?php
/*
bWAPP, or a buggy web application, is a free and open source deliberately insecure web application.
It helps security enthusiasts, developers and students to discover and to prevent web vulnerabilities.
bWAPP covers all major known web vulnerabilities, including all risks from the OWASP Top 10 project!
It is for security-testing and educational purposes only.
Enjoy!
Malik Mesellem
Twitter: @MME_IT
bWAPP is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (http://creativecommons.org/licenses/by-nc-nd/4.0/). Copyright © 2014 MME BVBA. All rights reserved.
*/
include("security.php");
include("security_level_check.php");
include("functions_external.php");
include("selections.php");
if(isset($_POST["form"]))
{
$file_error = "";
switch($_COOKIE["security_level"])
{
case "0" :
move_uploaded_file($_FILES["file"]["tmp_name"], "images/" . $_FILES["file"]["name"]);
break;
case "1" :
$file_error = file_upload_check_1($_FILES["file"]);
if(!$file_error)
{
move_uploaded_file($_FILES["file"]["tmp_name"], "images/" . $_FILES["file"]["name"]);
}
break;
case "2" :
$file_error = file_upload_check_2($_FILES["file"], array("jpg","png"));
if(!$file_error)
{
move_uploaded_file($_FILES["file"]["tmp_name"], "/data/images/" . $_FILES["file"]["name"]);
⇐ 외부에서 접근할 수 없는 경로에 파일을 저장하도록 수정
}
break;
default :
move_uploaded_file($_FILES["file"]["tmp_name"],"images/" . $_FILES["file"]["name"]);
break;
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!--<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Architects+Daughter">-->
<link rel="stylesheet" type="text/css" href="stylesheets/stylesheet.css" media="screen" />
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
<!--<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>-->
<script src="js/html5.js"></script>
<title>bWAPP - Unrestricted File Upload</title>
</head>
<body>
<header>
<h1>bWAPP</h1>
<h2>an extremely buggy web app !</h2>
</header>
<div id="menu">
<table>
<tr>
<td><a href="portal.php">Bugs</a></td>
<td><a href="password_change.php">Change Password</a></td>
<td><a href="user_extra.php">Create User</a></td>
<td><a href="security_level_set.php">Set Security Level</a></td>
<td><a href="reset.php" onclick="return confirm('All settings will be cleared. Are you sure?');">Reset</a></td>
<td><a href="credits.php">Credits</a></td>
<td><a href="http://itsecgames.blogspot.com" target="_blank">Blog</a></td>
<td><a href="logout.php" onclick="return confirm('Are you sure you want to leave?');">Logout</a></td>
<td><font color="red">Welcome <?php if(isset($_SESSION["login"])){echo ucwords($_SESSION["login"]);}?></font></td>
</tr>
</table>
</div>
<div id="main">
<h1>Unrestricted File Upload</h1>
<form action="<?php echo($_SERVER["SCRIPT_NAME"]);?>" method="POST" enctype="multipart/form-data">
<p><label for="file">Please upload an image:</label><br />
<input type="file" name="file"></p>
<input type="hidden" name="MAX_FILE_SIZE" value="10">
<!-- <input type="hidden" name="MAX_FILE_SIZE" value="100000"> -->
<input type="submit" name="form" value="Upload">
</form>
<br />
<?php
if(isset($_POST["form"]))
{
if(!$file_error)
{
echo "The image has been uploaded <a href=\"/data/images/" . $_FILES["file"]["name"] . "\" target=\"_blank\">here</a>.";
echo "<img src=\"/data/images/" . $_FILES["file"]["name"] . "\">";
} ⇐ 업로드된 파일에 대한 링크(<a>)와 출력(<img>)을 저장된 경로로 수정
→ 주소를 통해서 접근할 수 없으므로 링크도 출력도 되지 않음
else
{
echo "<font color=\"red\">" . $file_error . "</font>";
}
}
?>
</div>
<div id="side">
<a href="http://twitter.com/MME_IT" target="blank_" class="button"><img src="./images/twitter.png"></a>
<a href="http://be.linkedin.com/in/malikmesellem" target="blank_" class="button"><img src="./images/linkedin.png"></a>
<a href="http://www.facebook.com/pages/MME-IT-Audits-Security/104153019664877" target="blank_" class="button"><img src="./images/facebook.png"></a>
<a href="http://itsecgames.blogspot.com" target="blank_" class="button"><img src="./images/blogger.png"></a>
</div>
<div id="disclaimer">
<p>bWAPP is licensed under <a rel="license" href="http://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank"><img style="vertical-align:middle" src="./images/cc.png"></a> © 2014 MME BVBA / Follow <a href="http://twitter.com/MME_IT" target="_blank">@MME_IT</a> on Twitter and ask for our cheat sheet, containing all solutions! / Need an exclusive <a href="http://www.mmebvba.com" target="_blank">training</a>?</p>
</div>
<div id="bee">
<img src="./images/bee_1.png">
</div>
<div id="security_level">
<form action="<?php echo($_SERVER["SCRIPT_NAME"]);?>" method="POST">
<label>Set your security level:</label><br />
<select name="security_level">
<option value="0">low</option>
<option value="1">medium</option>
<option value="2">high</option>
</select>
<button type="submit" name="form_security_level" value="submit">Set</button>
<font size="4">Current: <b><?php echo $security_level?></b></font>
</form>
</div>
<div id="bug">
<form action="<?php echo($_SERVER["SCRIPT_NAME"]);?>" method="POST">
<label>Choose your bug:</label><br />
<select name="bug">
<?php
// Lists the options from the array 'bugs' (bugs.txt)
foreach ($bugs as $key => $value)
{
$bug = explode(",", trim($value));
// Debugging
// echo "key: " . $key;
// echo " value: " . $bug[0];
// echo " filename: " . $bug[1] . "<br />";
echo "<option value='$key'>$bug[0]</option>";
}
?>
</select>
<button type="submit" name="form_bug" value="submit">Hack</button>
</form>
</div>
</body>
</html>
bee@bee-box:~$ sudo mkdir -p /data/images
bee@bee-box:~$ ls /data/images
bee@bee-box:~$ sudo chown root:www-data /data/images
bee@bee-box:~$ sudo chmod 777 /data/imageskali 가상머신에서 이미지 파일을 업로드
⇒ 웹 루트 디렉터리 아래에 존재하지 않는 디렉터리와 파일을 참조하므로 링크도 동작하지 않고 이미지도 나타나지 않음
bee@bee-box:~$ ls /data/images
dog.jpg ⇐ 지정한 디렉터리에 파일이 저장된 것을 확인
bee@bee-box:~$ sudo gedit /var/www/bWAPP/download.php
<?php
$target_Dir = "/data/images/";
$file = $_GET['file'];
$down = $target_Dir.$file;
$filesize = filesize($down);
if(file_exists($down)){
header("Content-Type:application/octet-stream");
header("Content-Disposition:attachment;filename=$file");
header("Content-Transfer-Encoding:binary");
header("Content-Length:".filesize($target_Dir.$file));
header("Cache-Control:cache,must-revalidate");
header("Pragma:no-cache");
header("Expires:0");
if(is_file($down)){
$fp = fopen($down,"r");
while(!feof($fp)){
$buf = fread($fp,8096);
$read = strlen($buf);
print($buf);
flush();
}
fclose($fp);
}
} else{
?><script>alert("존재하지 않는 파일입니다.")</script><?
}
?>
if(isset($_POST["form"]))
{
if(!$file_error)
{
/*
echo "The image has been uploaded <a href=\"/data/images/" . $_FILES["file"]["name"] . "\" target=\"_blank\">here</a>.";
echo "<img src=\"/data/images/" . $_FILES["file"]["name"] . "\">";
*/
echo "The image has been uploaded <a href=\"download.php?file=" . $_FILES["file"]["name"] . "\" target=\"_blank\">here</a>.";
echo "<img src=\"download.php?file=" . $_FILES["file"]["name"] . "\">";
}
else
{
echo "<font color=\"red\">" . $file_error . "</font>";
}
}

┌──(kali㉿kali)-[~]
└─$ curl -v http://bee.box/bWAPP/download.php?file=../../../../../../../etc/passwd
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~
* Trying 192.168.40.130:80... 경로조작 문자열 시스템 파일 경로
* Connected to bee.box (192.168.40.130) port 80
> GET /bWAPP/download.php?file=../../../../../../../etc/passwd HTTP/1.1
> Host: bee.box
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 19 Mar 2024 04:41:12 GMT
< Server: Apache/2.2.8 (Ubuntu) DAV/2 mod_fastcgi/2.4.6 PHP/5.2.4-2ubuntu5 with Suhosin-Patch mod_ssl/2.2.8 OpenSSL/0.9.8g
< X-Powered-By: PHP/5.2.4-2ubuntu5
< Content-Disposition: attachment;filename=../../../../../../../etc/passwd
< Content-Transfer-Encoding: binary
< Content-Length: 2217
< Cache-Control: cache,must-revalidate
< Pragma: no-cache
< Expires: 0
< Content-Type: application/octet-stream
<
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
libuuid:x:100:101::/var/lib/libuuid:/bin/sh
dhcp:x:101:102::/nonexistent:/bin/false
syslog:x:102:103::/home/syslog:/bin/false
klog:x:103:104::/home/klog:/bin/false
hplip:x:104:7:HPLIP system user,,,:/var/run/hplip:/bin/false
avahi-autoipd:x:105:113:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false
gdm:x:106:114:Gnome Display Manager:/var/lib/gdm:/bin/false
pulse:x:107:116:PulseAudio daemon,,,:/var/run/pulse:/bin/false
messagebus:x:108:119::/var/run/dbus:/bin/false
avahi:x:109:120:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false
polkituser:x:110:122:PolicyKit,,,:/var/run/PolicyKit:/bin/false
haldaemon:x:111:123:Hardware abstraction layer,,,:/var/run/hald:/bin/false
bee:x:1000:1000:bee,,,:/home/bee:/bin/bash ⇐ beebox 웹 서버 사용자 계정
mysql:x:112:124:MySQL Server,,,:/var/lib/mysql:/bin/false
sshd:x:113:65534::/var/run/sshd:/usr/sbin/nologin
dovecot:x:114:126:Dovecot mail server,,,:/usr/lib/dovecot:/bin/false
smmta:x:115:127:Mail Transfer Agent,,,:/var/lib/sendmail:/bin/false
smmsp:x:116:128:Mail Submission Program,,,:/var/lib/sendmail:/bin/false
neo:x:1001:1001::/home/neo:/bin/sh
alice:x:1002:1002::/home/alice:/bin/sh
thor:x:1003:1003::/home/thor:/bin/sh
wolverine:x:1004:1004::/home/wolverine:/bin/sh
johnny:x:1005:1005::/home/johnny:/bin/sh
selene:x:1006:1006::/home/selene:/bin/sh
postfix:x:117:129::/var/spool/postfix:/bin/false
proftpd:x:118:65534::/var/run/proftpd:/bin/false
ftp:x:119:65534::/home/ftp:/bin/false
snmp:x:120:65534::/var/lib/snmp:/bin/false
ntp:x:121:131::/home/ntp:/bin/false
* Connection #0 to host bee.box left intact
https://www.kisa.or.kr/2060204/form?postSeq=13&lang_type=KO#fnPostAttachDownload
외부 입력값이 자원(파일, 소켓, 포트, 연결 ...) 식별자로 사용되는 경우
그 값을 검증, 제한하지 않고 사용하지 않으면 권한 밖 자원에 대해 접근할 수 있고 자원을 선점하거나 충돌시켜 정상적인 서비스를 방해
from django.urls import path
from . import views
app_name = 'pybo'
urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name="detail"),
path('answer/create/<int:question_id>', views.answer_create, name='answer_create'),
path('question/create/', views.question_create, name='question_create'),
path('download/', views.download, name='download'),
]
from django.shortcuts import render, get_object_or_404, redirect
from .models import Question
from .forms import QuestionForm
from django.utils import timezone
import subprocess
import os
def index(request):
question_list = Question.objects.order_by('-create_date')
context = { 'question_list': question_list }
return render(request, 'pybo/question_list.html', context)
def download(request):
request_file = request.GET.get('request_file')
(filename, file_ext) = os.path.splitext(request_file)
file_ext = file_ext.lower()
if file_ext not in ['.txt', '.csv']:
return render(request, 'pybo/error.html', {'error':'파일을 열 수 없습니다.'})
with open(request_file) as f:
data = f.read()
return render(request, 'pybo/success.html', {'data':data})
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
{{ error }}
</div>
{% endblock %}
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
{{ data }}
</div>
{% endblock %}
c:\Users\crpark> echo "Hello, Python" > c:\temp\hello.txt ⇐ 웹 페이지에서 접근할 수 있는 파일
c:\Users\crpark> type c:\temp\hello.txt
"Hello, Python"
c:\Users\crpark> echo Python user information > c:\python\userinfo.txt ⇐ 웹 페이지에서 접근할 수 없는 파일
c:\Users\crpark> type c:\python\userinfo.txt
Python user information
(mysite) c:\python\projects\mysite> python manage.py runserver

def download(request):
request_file = request.GET.get('request_file')
(filename, file_ext) = os.path.splitext(request_file)
file_ext = file_ext.lower()
if file_ext not in ['.txt', '.csv']:
return render(request, 'pybo/error.html', {'error':'파일을 열 수 없습니다.'})
request_file = f"c:\\temp\\{filename}{file_ext}"
with open(request_file) as f:
data = f.read()
return render(request, 'pybo/success.html', {'data':data})



def download(request):
request_file = request.GET.get('request_file')
(filename, file_ext) = os.path.splitext(request_file)
file_ext = file_ext.lower()
if file_ext not in ['.txt', '.csv']:
return render(request, 'pybo/error.html', {'error':'파일을 열 수 없습니다.'})
filename = filename.replace('.', '')
filename = filename.replace('/', '')
filename = filename.replace('\\', '')
request_file = f"c:\\temp\\{filename}{file_ext}"
with open(request_file) as f:
data = f.read()
return render(request, 'pybo/success.html', {'data':data})

유형 1 : Reflective XSS (or Non-persistent XSS)
유형 2 : Persistent XSS (or Stored XSS)
유형 3 : DOM XSS (or Client-Side XSS)
pybo\urls.py
from django.urls import path
from . import views
app_name = 'pybo'
urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name="detail"),
path('answer/create/<int:question_id>', views.answer_create, name='answer_create'),
path('question/create/', views.question_create, name='question_create'),
path('download/', views.download, name='download'),
path('execute/app/<str:app_name>', views.execute_app, name='execute_app'),
]
pybo\views.py 파일에 함수를 추가
def execute_app(request, app_name):
os.system(app_name)
return render(request, 'pybo/success.html', {'data': f'{app_name}을 실행했습니다.'})


ALLOW_PROGRAM = ['notepad', 'calc'] # 허용 목록(white list)
def execute_app(request, app_name):
if app_name in ALLOW_PROGRAM:
os.system(app_name)
return render(request, 'pybo/success.html', {'data': f'{app_name}을 실행했습니다.'})
else:
return render(request, 'pybo/error.html', {'error': f'{app_name}을 실행할 수 없습니다.'})

urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name="detail"),
path('answer/create/<int:question_id>', views.answer_create, name='answer_create'),
path('question/create/', views.question_create, name='question_create'),
path('download/', views.download, name='download'),
path('execute/app/<str:app_name>', views.execute_app, name='execute_app'),
path('execute/cmd/', views.execute_cmd, name='execute_cmd'),
]
from django.utils.safestring import mark_safe
def execute_cmd(request):
cmd = request.GET.get('cmd','')
result = subprocess.check_output(cmd, shell=True, encoding='MS949')
data = mark_safe(f'<pre>{cmd}을 실행했습니다.\n {result}</pre>')
return render(request, 'pybo/success.html', {'data': data})
ALLOW_COMMANDS = ['dir', 'whoami']
def execute_cmd(request):
cmd = request.GET.get('cmd','')
if cmd in ALLOW_COMMANDS:
result = subprocess.check_output(cmd, shell=True, encoding='MS949')
data = mark_safe(f'<pre>{cmd}을 실행했습니다.\n {result}</pre>')
return render(request, 'pybo/success.html', {'data': data})
else:
return render(request, 'pybo/error.html', {'error': f'{cmd}는 실행할 수 없습니다.'})
https://developer.mozilla.org/ko/docs/Web/HTTP/Redirections

